diff --git a/.circleci/config.yml b/.circleci/config.yml index 095650aae02d..74815818582f 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 @@ -1012,7 +1008,7 @@ jobs: command: ./development/shellcheck.sh test-lint-lockfile: - executor: node-browsers-medium + executor: node-browsers-medium-plus steps: - run: *shallow-git-clone-and-enable-vnc - run: sudo corepack enable @@ -1254,7 +1250,7 @@ jobs: command: mv ./builds-test-flask-mv2 ./builds - run: name: test:e2e:firefox:flask - command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1397,7 +1393,7 @@ jobs: command: mv ./builds-test-mv2 ./builds - run: name: test:e2e:firefox - command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox no_output_timeout: 5m - store_artifacts: path: test-artifacts 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/.circleci/scripts/test-run-e2e-timeout-minutes.ts b/.circleci/scripts/test-run-e2e-timeout-minutes.ts index 1fc06696712a..c539133b0c60 100644 --- a/.circleci/scripts/test-run-e2e-timeout-minutes.ts +++ b/.circleci/scripts/test-run-e2e-timeout-minutes.ts @@ -2,7 +2,7 @@ import { filterE2eChangedFiles } from '../../test/e2e/changedFilesUtil'; const changedOrNewTests = filterE2eChangedFiles(); -//15 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes -const extraTime = Math.min(15 + changedOrNewTests.length * 3, 30); +// 20 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes +const extraTime = Math.min(20 + changedOrNewTests.length * 3, 30); console.log(extraTime); diff --git a/.depcheckrc.yml b/.depcheckrc.yml index bafacc56c918..d0d6eac5b5bc 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -80,6 +80,7 @@ ignores: - '@babel/plugin-transform-logical-assignment-operators' # trezor - 'ts-mixer' + - '@testing-library/dom' # files depcheck should not parse ignorePatterns: diff --git a/.eslintrc.js b/.eslintrc.js index 64c51bd1e503..846158a741ef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -307,8 +307,9 @@ module.exports = { { files: [ '**/__snapshots__/*.snap', - 'app/scripts/controllers/app-state.test.js', + 'app/scripts/controllers/app-state-controller.test.ts', 'app/scripts/controllers/mmi-controller.test.ts', + 'app/scripts/controllers/alert-controller.test.ts', 'app/scripts/metamask-controller.actions.test.js', 'app/scripts/detect-multiple-instances.test.js', 'app/scripts/controllers/bridge.test.ts', @@ -316,7 +317,8 @@ module.exports = { 'app/scripts/controllers/swaps/**/*.test.ts', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', - 'app/scripts/controllers/preferences.test.js', + 'app/scripts/controllers/preferences-controller.test.ts', + 'app/scripts/controllers/account-tracker-controller.test.ts', 'app/scripts/lib/**/*.test.js', 'app/scripts/metamask-controller.test.js', 'app/scripts/migrations/*.test.js', diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index b4979c8f3e7b..f8e24692e8fe 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -2,12 +2,14 @@ name: Fitness Functions CI on: pull_request: - types: [assigned, opened, synchronize, reopened] + types: + - opened + - reopened + - synchronize jobs: build: runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d14fefe82717..f3cc68bebcec 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,16 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest needs: - check-workflows + - run-tests outputs: PASSED: ${{ steps.set-output.outputs.PASSED }} steps: @@ -37,7 +49,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..9ca9f02e2ae5 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,58 @@ +# This GitHub action will checkout and scan third party code. +# Please ensure that any changes to this action do not perform +# actions that may result in code from that branch being executed +# such as installing dependencies or running build scripts. + +name: SonarCloud + +on: + workflow_run: + workflows: + - Run tests + types: + - completed + +permissions: + actions: read + +jobs: + sonarcloud: + # Only scan code from non-forked repositories that have passed the tests + # This will skip scanning the code for forks, but will run for the main repository on PRs from forks + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.repository.fork == false }} + name: SonarCloud + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} # Use the repository that triggered the workflow + ref: ${{ github.event.workflow_run.head_branch }} # Use the branch that triggered the workflow + 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 + github-token: ${{ github.token }} # This is required when downloading artifacts from a different repository or from a different workflow run. + run-id: ${{ github.event.workflow_run.id }} # Use the workflow id that triggered the workflow + + - name: Download sonar-project.properties + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: MetaMask/metamask-extension + run: | + sonar_project_properties=$(gh api -H "Accept: application/vnd.github.raw" "repos/$REPOSITORY/contents/sonar-project.properties") + if [ -z "$sonar_project_properties" ]; then + echo "::error::sonar-project.properties not found in $REPOSITORY. Please make sure this file exists on the default branch." + exit 1 + fi + echo "$sonar_project_properties" > sonar-project.properties + + - 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.3.0-57b3d695bb.patch b/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch new file mode 100644 index 000000000000..7a5837cd4818 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.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/.yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch b/.yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch deleted file mode 100644 index 692db45490f5..000000000000 --- a/.yarn/patches/@metamask-signature-controller-npm-6.1.2-f60d8a4960.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/dist/SignatureController.js b/dist/SignatureController.js -index 8ac1b2158ff4564fe2f942ca955bd337d78a94ef..c6552d874d830e610fcff791eb0f87f51fae1770 100644 ---- a/dist/SignatureController.js -+++ b/dist/SignatureController.js -@@ -278,6 +278,9 @@ _SignatureController_isEthSignEnabled = new WeakMap(), _SignatureController_getA - const messageParamsWithId = Object.assign(Object.assign(Object.assign({}, messageParams), { metamaskId: messageId }), (version && { version })); - const signaturePromise = messageManager.waitForFinishStatus(messageParamsWithId, messageName); - try { -+ signaturePromise.catch(() => { -+ // Expecting reject error but throwing manually rather than waiting -+ }); - // Signature request is proposed to the user - __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_addLog).call(this, signTypeForLogger, logging_controller_1.SigningStage.Proposed, messageParamsWithId); - const acceptResult = yield __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_requestApproval).call(this, messageParamsWithId, approvalType); -@@ -287,7 +290,7 @@ _SignatureController_isEthSignEnabled = new WeakMap(), _SignatureController_getA - // User rejected the signature request - __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_addLog).call(this, signTypeForLogger, logging_controller_1.SigningStage.Rejected, messageParamsWithId); - __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_cancelAbstractMessage).call(this, messageManager, messageId); -- throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the request.'); -+ throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest(`MetaMask ${messageName} Signature: User denied message signature.`); - } - yield signMessage(messageParamsWithId, signingOpts); - const signatureResult = yield signaturePromise; diff --git a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch deleted file mode 100644 index 3361025d4860..000000000000 --- a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch +++ /dev/null @@ -1,120 +0,0 @@ -diff --git a/dist/ui.cjs b/dist/ui.cjs -index 300fe9e97bba85945e3c2d200e736987453f8268..d6fa322e2b3629f41d653b91db52c3db85064276 100644 ---- a/dist/ui.cjs -+++ b/dist/ui.cjs -@@ -200,13 +200,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - (0, utils_1.assert)(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- (0, utils_1.assert)(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ (0, utils_1.assert)(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ (0, utils_1.assert)(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ (0, utils_1.assert)(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.cjs.map b/dist/ui.cjs.map -index 71b5ecb9eb8bc8bdf919daccf24b25737ee69819..6d6e56cd7fea85e4d477c0399506a03d465ca740 100644 ---- a/dist/ui.cjs.map -+++ b/dist/ui.cjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";;;;AACA,mDAA+C;AAa/C,iDAiBiC;AACjC,2CAKyB;AACzB,mCAA2C;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,uBAAC,UAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,uBAAC,UAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,uBAAC,YAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,IAAA,cAAK,EAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,IAAA,mBAAU,EAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAhCD,0CAgCC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAA,cAAM,EACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,oBAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,uBAAC,YAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,oBAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,uBAAC,cAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,uBAAC,UAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,oBAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,uBAAC,WAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,oBAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,uBAAC,WAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,uBAAC,WAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,oBAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,uBAAC,SAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,GAAG;gBACf,OAAO,CACL,uBAAC,SAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,uBAAC,UAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,IAAA,wBAAgB,EAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAxFD,gEAwFC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,IAAA,cAAK,EAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,IAAA,mBAAU,EAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAA,cAAM,EACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;QAEzE,IAAA,cAAM,EAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAtBD,oCAsBC;AAED;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AATD,8CASC;AAED;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAXD,4CAWC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,oBAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,oBAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,oBAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AArBD,gDAqBC;AAED;;;;;GAKG;AACH,SAAgB,WAAW,CACzB,OAAgB;IAIhB,OAAO,IAAA,mBAAW,EAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAND,kCAMC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAdD,wCAcC;AAED;;;;;;;GAOG;AACH,SAAgB,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,IAAA,mBAAW,EAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,CAAC;QACzB,IAAA,mBAAW,EAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,IAAA,qBAAa,EAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAtCD,0BAsCC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,IAAA,mBAAW,EAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC;AAzBD,oCAyBC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";;;;AACA,mDAA+C;AAa/C,iDAiBiC;AACjC,2CAKyB;AACzB,mCAA2C;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,uBAAC,UAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,uBAAC,UAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,uBAAC,YAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,IAAA,cAAK,EAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,IAAA,mBAAU,EAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAhCD,0CAgCC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAA,cAAM,EACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,oBAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,uBAAC,YAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,oBAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,uBAAC,cAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,uBAAC,UAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,oBAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,uBAAC,WAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,oBAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,uBAAC,WAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,uBAAC,WAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,oBAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,uBAAC,SAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,oBAAQ,CAAC,GAAG;gBACf,OAAO,CACL,uBAAC,SAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,oBAAQ,CAAC,OAAO;gBACnB,OAAO,uBAAC,aAAO,KAAG,CAAC;YAErB,KAAK,oBAAQ,CAAC,IAAI;gBAChB,OAAO,uBAAC,UAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,IAAA,wBAAgB,EAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAxFD,gEAwFC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,IAAA,cAAK,EAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,IAAA,mBAAU,EAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAA,cAAM,EACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAA,cAAM,EAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;gBAChC,MAAM,IAAI,GAAG,WAAW,QAAQ,EAAE,CAAC;gBACnC,IAAA,cAAM,EAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;YACvE,CAAC;YAED,OAAO;QACT,CAAC;QAED,IAAA,cAAM,EAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AA/BD,oCA+BC;AAED;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AATD,8CASC;AAED;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAXD,4CAWC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,oBAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,oBAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,oBAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AArBD,gDAqBC;AAED;;;;;GAKG;AACH,SAAgB,WAAW,CACzB,OAAgB;IAIhB,OAAO,IAAA,mBAAW,EAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAND,kCAMC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAdD,wCAcC;AAED;;;;;;;GAOG;AACH,SAAgB,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,IAAA,mBAAW,EAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,CAAC;QACzB,IAAA,mBAAW,EAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,IAAA,qBAAa,EAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAtCD,0BAsCC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,IAAA,mBAAW,EAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC;AAzBD,oCAyBC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -diff --git a/dist/ui.d.cts b/dist/ui.d.cts -index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644 ---- a/dist/ui.d.cts -+++ b/dist/ui.d.cts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map -index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644 ---- a/dist/ui.d.cts.map -+++ b/dist/ui.d.cts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.d.mts b/dist/ui.d.mts -index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644 ---- a/dist/ui.d.mts -+++ b/dist/ui.d.mts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map -index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644 ---- a/dist/ui.d.mts.map -+++ b/dist/ui.d.mts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.mjs b/dist/ui.mjs -index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644 ---- a/dist/ui.mjs -+++ b/dist/ui.mjs -@@ -195,13 +195,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ assert(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ assert(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map -index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644 ---- a/dist/ui.mjs.map -+++ b/dist/ui.mjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,4BAA4B;AAa/C,OAAO,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,GAAG,EACH,MAAM,EACN,OAAO,EACR,gCAAgC;AACjC,OAAO,EACL,MAAM,EACN,gBAAgB,EAChB,WAAW,EACX,aAAa,EACd,wBAAwB;AACzB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,KAAC,IAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,KAAC,MAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,UAAU,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,CACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,QAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,QAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,KAAC,QAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,QAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,KAAC,KAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,QAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,KAAC,KAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,QAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,KAAC,GAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,QAAQ,CAAC,GAAG;gBACf,OAAO,CACL,KAAC,GAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,KAAC,IAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,UAAU,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,CACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;QAEzE,MAAM,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,QAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,QAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,OAAgB;IAIhB,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,4BAA4B;AAa/C,OAAO,EACL,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,GAAG,EACH,MAAM,EACN,OAAO,EACR,gCAAgC;AACjC,OAAO,EACL,MAAM,EACN,gBAAgB,EAChB,WAAW,EACX,aAAa,EACd,wBAAwB;AACzB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe;AAG3C,MAAM,eAAe,GAAG,KAAM,CAAC,CAAC,QAAQ;AACxC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAA6C;IACrE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAO,QAAgB;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAmC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,MAAe;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,KAAY;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAAI,CAAC;QAClE,CAAC;QAED,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,IAAI,CAAC;QAEpB,KAAK,QAAQ;YACX,OAAO,CACL,KAAC,IAAI,cAED,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACR,GAEd,CACR,CAAC;QAEJ,KAAK,IAAI;YACP,OAAO,CACL,KAAC,MAAM,cAEH,sBAAsB;gBACpB,0DAA0D;gBAC1D,iEAAiE;gBACjE,mCAAmC;gBACnC,KAAK,CAAC,MAAiB,CACN,GAEd,CACV,CAAC;QAEJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa;IAEb,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,GACZ,EAAE,CAAC;IAEL,UAAU,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,KAAyB,CAAC;YAC7C,yFAAyF;YACzF,QAAQ,CAAC,IAAI,CACX,GAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAKpC,CACL,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAI7C,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAAoB;IACrD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,CACJ,QAAQ,IAAI,eAAe,EAC3B,gDACE,eAAe,GAAG,IACpB,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,0BAA0B,CACxC,eAA0B;IAE1B,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3C;;;;;;OAMG;IACH,SAAS,UAAU,CAAC,SAAoB;QACtC,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAE/C,KAAK,QAAQ,CAAC,MAAM;gBAClB,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,EAC5C,IAAI,EAAE,SAAS,CAAC,UAAU,YAEzB,SAAS,CAAC,KAAK,GACT,CACV,CAAC;YAEJ,KAAK,QAAQ,CAAC,QAAQ;gBACpB,OAAO,CACL,KAAC,QAAQ,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,GAAI,CACrE,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,SAAS,CAAC,IAAI,YACvB,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAC3C,CACR,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,IAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEhD,KAAK,QAAQ,CAAC,KAAK;gBACjB,qEAAqE;gBACrE,OAAO,KAAC,KAAK,IAAC,GAAG,EAAE,SAAS,CAAC,KAAK,GAAI,CAAC;YAEzC,KAAK,QAAQ,CAAC,KAAK;gBACjB,OAAO,CACL,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,YACnD,KAAC,KAAK,IACJ,IAAI,EAAE,SAAS,CAAC,IAAI,EACpB,IAAI,EAAE,SAAS,CAAC,SAAS,EACzB,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,WAAW,EAAE,SAAS,CAAC,WAAW,GAClC,GACI,CACT,CAAC;YAEJ,KAAK,QAAQ,CAAC,KAAK;gBACjB,sCAAsC;gBACtC,OAAO,CACL,KAAC,GAAG,IAAC,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,GAAI,CACnE,CAAC;YAEJ,KAAK,QAAQ,CAAC,GAAG;gBACf,OAAO,CACL,KAAC,GAAG,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,YACpD,UAAU,CAAC,SAAS,CAAC,KAAK,CAAgB,GACvC,CACP,CAAC;YAEJ,KAAK,QAAQ,CAAC,OAAO;gBACnB,OAAO,KAAC,OAAO,KAAG,CAAC;YAErB,KAAK,QAAQ,CAAC,IAAI;gBAChB,OAAO,KAAC,IAAI,cAAE,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAQ,CAAC;YAEtE,4BAA4B;YAC5B;gBACE,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,oDAAoD;IACpD,UAAU,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,KAAoB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,gBAA0C;IAE1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,CACJ,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EACxC,4BAA4B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5D,CAAC;QAEF,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;gBAChC,MAAM,IAAI,GAAG,WAAW,QAAQ,EAAE,CAAC;gBACnC,MAAM,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;YACvE,CAAC;YAED,OAAO;QACT,CAAC;QAED,MAAM,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gBACE,KAAK,EAAE,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBACpD,EAAE,CACH,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,gBAA0C;IAE1C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAgB,EAChB,gBAA0C;IAE1C,OAAO,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1B,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAoB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,CAAC,KAAK;YACjB,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM;YAC9B,oFAAoF;YACpF,qEAAqE;YACrE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAC7C,CAAC,CACF,CAAC;QAEJ,KAAK,QAAQ,CAAC,GAAG;YACf,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE7C,KAAK,QAAQ,CAAC,IAAI;YAChB,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAEhC;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,OAAgB;IAIhB,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,KAA2C;IACjE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAmB;IAChD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,uEAAuE;YACvE,2DAA2D;YAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAA+B,EAC/B,QAAgE,EAChE,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAmB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAClE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IACE,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EACnC,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAA8B;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACxD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,WAAW,GAAG,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG,MAAM,GAAG,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAA0B,CAAC;IACnD,MAAM,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,IAAI,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,QAAoB,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CACvC,KAAK,CACN,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,eAAe,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,eAAe,EAAE,CAAC;AAC1E,CAAC","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file diff --git a/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch b/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch deleted file mode 100644 index 515ff415b4df..000000000000 --- a/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch +++ /dev/null @@ -1,57 +0,0 @@ -diff --git a/lib/impl/core-in-iframe.js b/lib/impl/core-in-iframe.js -index c47cf3bff860d6b1855341c00b80fc6c40f9d6d5..0151bcaac6689ecb26f1b4575ece4f3760ca1b87 100644 ---- a/lib/impl/core-in-iframe.js -+++ b/lib/impl/core-in-iframe.js -@@ -116,7 +116,9 @@ class CoreInIframe { - this._log.enabled = !!this._settings.debug; - window.addEventListener('message', this.boundHandleMessage); - window.addEventListener('unload', this.boundDispose); -- await iframe.init(this._settings); -+ var modifiedSettings = Object.assign({}, this._settings); -+ modifiedSettings.env = 'webextension'; -+ await iframe.init(modifiedSettings); - if (this._settings.sharedLogger !== false) { - iframe.initIframeLogger(); - } -@@ -132,7 +134,9 @@ class CoreInIframe { - } - this._popupManager.request(); - try { -- await this.init(this._settings); -+ var modifiedSettings = Object.assign({}, this._settings); -+ modifiedSettings.env = 'webextension'; -+ await this.init(modifiedSettings); - } - catch (error) { - if (this._popupManager) { -diff --git a/lib/popup/index.js b/lib/popup/index.js -index 9b13c370a5ac8b4e4fc0315ed40cdf615d0bb0cb..595a7d9e1aa397b3aa53ba5d75e4ccf22a61bcf1 100644 ---- a/lib/popup/index.js -+++ b/lib/popup/index.js -@@ -229,10 +229,12 @@ class PopupManager extends events_1.default { - } - else if (message.type === events_2.POPUP.LOADED) { - this.handleMessage(message); -+ var modifiedSettings = Object.assign({}, this.settings); -+ modifiedSettings.env = 'webextension'; - this.channel.postMessage({ - type: events_2.POPUP.INIT, - payload: { -- settings: this.settings, -+ settings: modifiedSettings, - useCore: true, - }, - }); -@@ -292,9 +294,11 @@ class PopupManager extends events_1.default { - this.popupPromise = undefined; - } - (_b = this.iframeHandshakePromise) === null || _b === void 0 ? void 0 : _b.promise.then(payload => { -+ var modifiedSettings = Object.assign({}, this.settings); -+ modifiedSettings.env = 'webextension'; - this.channel.postMessage({ - type: events_2.POPUP.INIT, -- payload: Object.assign(Object.assign({}, payload), { settings: this.settings }), -+ payload: Object.assign(Object.assign({}, payload), { settings: modifiedSettings }), - }); - }); - } diff --git a/.yarnrc.yml b/.yarnrc.yml index fb335f532861..f4d8fc7fa471 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -43,12 +43,6 @@ npmAuditIgnoreAdvisories: # not appear to be used. - 1092461 - # Issue: Sentry SDK Prototype Pollution gadget in JavaScript SDKs - # URL: https://github.com/advisories/GHSA-593m-55hh-j8gv - # Not easily fixed in this version, will be fixed in v12.5.0 - # Minimally effects the extension due to usage of LavaMoat + SES lockdown. - - 1099839 - # Temp fix for https://github.com/MetaMask/metamask-extension/pull/16920 for the sake of 11.7.1 hotfix # This will be removed in this ticket https://github.com/MetaMask/metamask-extension/issues/22299 - 'ts-custom-error (deprecation)' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b33f07fb3d5..fda366ce5558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,219 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.6.0] +### Uncategorized +- ci: reduced Sentry frequency on CircleCI develop ([#27912](https://github.com/MetaMask/metamask-extension/pull/27912)) +- chore:Master sync ([#27935](https://github.com/MetaMask/metamask-extension/pull/27935)) +- Merge origin/develop into master-sync +- test: Completing missing step for import ERC1155 token origin dapp in existing E2E test ([#27680](https://github.com/MetaMask/metamask-extension/pull/27680)) +- fix: error in navigating between transaction when one of the transaction is approve all ([#27985](https://github.com/MetaMask/metamask-extension/pull/27985)) +- fix: Automatically expand first insight ([#27872](https://github.com/MetaMask/metamask-extension/pull/27872)) +- feat(metametrics): use specific `account_hardware_type` for OneKey devices ([#27296](https://github.com/MetaMask/metamask-extension/pull/27296)) +- feat: add migration 131 ([#27364](https://github.com/MetaMask/metamask-extension/pull/27364)) +- fix(snaps): Remove arrows of custom UI inputs ([#27953](https://github.com/MetaMask/metamask-extension/pull/27953)) +- chore: Disable account syncing in prod ([#27943](https://github.com/MetaMask/metamask-extension/pull/27943)) +- test: Remove delays from onboarding tests ([#27961](https://github.com/MetaMask/metamask-extension/pull/27961)) +- perf: Create custom trace to measure performance of opening the account list ([#27907](https://github.com/MetaMask/metamask-extension/pull/27907)) +- feat: add BTC send flow ([#27964](https://github.com/MetaMask/metamask-extension/pull/27964)) +- fix: flaky test `Confirmation Redesign ERC721 Approve Component Submit an Approve transaction @no-mmi Sends a type 2 transaction (EIP1559)` ([#27928](https://github.com/MetaMask/metamask-extension/pull/27928)) +- fix: lint-lockfile flaky job by changing resources from medium to medium-plus ([#27950](https://github.com/MetaMask/metamask-extension/pull/27950)) +- feat: add “Incomplete Asset Displayed” metric & fix: should only set default decimals if ERC20 ([#27494](https://github.com/MetaMask/metamask-extension/pull/27494)) +- feat: Convert AppStateController to typescript ([#27572](https://github.com/MetaMask/metamask-extension/pull/27572)) +- chore(deps): upgrade from json-rpc-engine to @metamask/json-rpc-engine ([#22875](https://github.com/MetaMask/metamask-extension/pull/22875)) +- feat: dapp initiated token transfer ([#27875](https://github.com/MetaMask/metamask-extension/pull/27875)) +- chore: bump signature controller to remove message managers ([#27787](https://github.com/MetaMask/metamask-extension/pull/27787)) +- chore: add testing-library/dom dependency ([#27493](https://github.com/MetaMask/metamask-extension/pull/27493)) +- test: [POM] Migrate contract interaction with snap account e2e tests to page object modal ([#27924](https://github.com/MetaMask/metamask-extension/pull/27924)) +- fix: bump message signing snap to support portfolio automatic connections ([#27936](https://github.com/MetaMask/metamask-extension/pull/27936)) +- fix: hide options menu that was being shown for preinstalled Snaps ([#27937](https://github.com/MetaMask/metamask-extension/pull/27937)) +- fix: bump `@metamask/ppom-validator` from `0.34.0` to `0.35.1` ([#27939](https://github.com/MetaMask/metamask-extension/pull/27939)) +- fix: add APE network icon ([#27841](https://github.com/MetaMask/metamask-extension/pull/27841)) +- feat: NFT permit simulations ([#27825](https://github.com/MetaMask/metamask-extension/pull/27825)) +- fix: fix currency display when tokenToFiatConversion rate is not avai… ([#27893](https://github.com/MetaMask/metamask-extension/pull/27893)) +- feat: convert AlertController to typescript ([#27764](https://github.com/MetaMask/metamask-extension/pull/27764)) +- feat(TXL-435): turn smart transactions on by default for new users ([#27885](https://github.com/MetaMask/metamask-extension/pull/27885)) +- feat: Add transaction flow and details sections ([#27654](https://github.com/MetaMask/metamask-extension/pull/27654)) +- fix: flaky test `Vault Decryptor Page is able to decrypt the vault pasting the text in the vault-decryptor webapp` ([#27921](https://github.com/MetaMask/metamask-extension/pull/27921)) +- chore: bump `@metamask/eth-snap-keyring` to version 4.4.0 ([#27864](https://github.com/MetaMask/metamask-extension/pull/27864)) +- fix: flaky tests `Add existing token using search renders the balance for the chosen token` ([#27853](https://github.com/MetaMask/metamask-extension/pull/27853)) +- feat(logging): add extension request logging and retrieval ([#27655](https://github.com/MetaMask/metamask-extension/pull/27655)) +- test: Update test-dapp to verison 8.7.0 ([#27816](https://github.com/MetaMask/metamask-extension/pull/27816)) +- fix: fall back to bundled chainlist ([#23392](https://github.com/MetaMask/metamask-extension/pull/23392)) +- fix: SonarCloud for forks ([#27700](https://github.com/MetaMask/metamask-extension/pull/27700)) +- fix(deps): update from eth-rpc-errors to @metamask/rpc-errors (cause edition) ([#24496](https://github.com/MetaMask/metamask-extension/pull/24496)) +- fix: swapQuotesError as a property in the reported metric ([#27712](https://github.com/MetaMask/metamask-extension/pull/27712)) +- chore: Bump Snaps packages ([#27376](https://github.com/MetaMask/metamask-extension/pull/27376)) +- chore: update @metamask/bitcoin-wallet-snap to 0.7.0 ([#27730](https://github.com/MetaMask/metamask-extension/pull/27730)) +- fix: Onboarding: Code style nits ([#27767](https://github.com/MetaMask/metamask-extension/pull/27767)) +- fix: updated edit modals ([#27623](https://github.com/MetaMask/metamask-extension/pull/27623)) +- feat: use asset pickers with network dropdown in cross-chain swaps page ([#27522](https://github.com/MetaMask/metamask-extension/pull/27522)) +- test: set ENABLE_MV3 automatically ([#27748](https://github.com/MetaMask/metamask-extension/pull/27748)) +- feat: Adding typed sign support for NFT permit ([#27796](https://github.com/MetaMask/metamask-extension/pull/27796)) +- fix: Contract Interaction - cannot read the property `text_signature` ([#27686](https://github.com/MetaMask/metamask-extension/pull/27686)) +- feat: Use requested permissions as default selected values for AmonHenV2 connection flow with case insensitive address comparison ([#27517](https://github.com/MetaMask/metamask-extension/pull/27517)) +- test: [POM] Migrate signature with snap account e2e tests to page object modal ([#27829](https://github.com/MetaMask/metamask-extension/pull/27829)) +- fix: flaky test `ERC1155 NFTs testdapp interaction should batch transfers ERC1155 token` ([#27897](https://github.com/MetaMask/metamask-extension/pull/27897)) +- chore: Master sync following v12.4.1 ([#27793](https://github.com/MetaMask/metamask-extension/pull/27793)) +- fix: flaky test `Permissions sets permissions and connect to Dapp` ([#27888](https://github.com/MetaMask/metamask-extension/pull/27888)) +- fix: flaky test `ERC721 NFTs testdapp interaction should prompt users to add their NFTs to their wallet (all at once)` ([#27889](https://github.com/MetaMask/metamask-extension/pull/27889)) +- fix: flaky test `Wallet Revoke Permissions should revoke eth_accounts permissions via test dapp` ([#27894](https://github.com/MetaMask/metamask-extension/pull/27894)) +- fix: flaky test `Snap Account Signatures and Disconnects can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)` ([#27887](https://github.com/MetaMask/metamask-extension/pull/27887)) +- test(mock-e2e): add private domains logic for the privacy report ([#27844](https://github.com/MetaMask/metamask-extension/pull/27844)) +- fix: SENTRY_DSN_FAKE problem ([#27881](https://github.com/MetaMask/metamask-extension/pull/27881)) +- chore: remove unused swaps code ([#27679](https://github.com/MetaMask/metamask-extension/pull/27679)) +- test(TXL-308): initial e2e for stx using swaps ([#27215](https://github.com/MetaMask/metamask-extension/pull/27215)) +- feat: upgrade assets-controllers to v38.3.0 ([#27755](https://github.com/MetaMask/metamask-extension/pull/27755)) +- fix: nonce value when there are multiple transactions in parallel ([#27874](https://github.com/MetaMask/metamask-extension/pull/27874)) +- fix: phishing test to not check c2 domains ([#27846](https://github.com/MetaMask/metamask-extension/pull/27846)) +- feat: use messenger in AccountTracker to get Preferences state ([#27711](https://github.com/MetaMask/metamask-extension/pull/27711)) +- fix: "Update Network: should update added rpc url for exis..." flaky tests ([#27437](https://github.com/MetaMask/metamask-extension/pull/27437)) +- feat: update copy for 'Default settings' ([#27821](https://github.com/MetaMask/metamask-extension/pull/27821)) +- fix: updated permissions flow copy changes ([#27658](https://github.com/MetaMask/metamask-extension/pull/27658)) +- fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi` ([#27834](https://github.com/MetaMask/metamask-extension/pull/27834)) +- fix: hackily wait longer for linea swap approval tx to increase chance of success ([#27810](https://github.com/MetaMask/metamask-extension/pull/27810)) +- fix: flaky test `MultiRpc: should select rpc from settings @no-mmi` ([#27858](https://github.com/MetaMask/metamask-extension/pull/27858)) +- perf: include custom traces in benchmark results ([#27701](https://github.com/MetaMask/metamask-extension/pull/27701)) +- fix: Reset nonce as network is switched ([#27789](https://github.com/MetaMask/metamask-extension/pull/27789)) +- fix: dismiss addToken modal for mmi ([#27855](https://github.com/MetaMask/metamask-extension/pull/27855)) +- fix(multichain): fix eth send flow (from dapp) when a btc account is selected ([#27566](https://github.com/MetaMask/metamask-extension/pull/27566)) +- chore: Add react-beautiful-dnd to deprecated packages list ([#27856](https://github.com/MetaMask/metamask-extension/pull/27856)) +- feat: Create a quality gate for typescript coverage ([#27717](https://github.com/MetaMask/metamask-extension/pull/27717)) +- feat: preferences controller to base controller v2 ([#27398](https://github.com/MetaMask/metamask-extension/pull/27398)) +- revert: use networkClientId to resolve chainId in PPOM Middleware ([#27570](https://github.com/MetaMask/metamask-extension/pull/27570)) +- feat: Added metrics for edit networks and accounts ([#27820](https://github.com/MetaMask/metamask-extension/pull/27820)) +- fix: no connected state for permissions page ([#27660](https://github.com/MetaMask/metamask-extension/pull/27660)) +- feat: remove phishing detection from onboarding Security group ([#27819](https://github.com/MetaMask/metamask-extension/pull/27819)) +- ci: Revert minimum E2E timeout to 20 minutes ([#27827](https://github.com/MetaMask/metamask-extension/pull/27827)) +- fix: disable balance checker for Sepolia in account tracker ([#27763](https://github.com/MetaMask/metamask-extension/pull/27763)) +- ci: Improve validation for `sentry:publish` script ([#26580](https://github.com/MetaMask/metamask-extension/pull/26580)) +- test: Fix Vault Decryptor Page e2e test on develop branch ([#27794](https://github.com/MetaMask/metamask-extension/pull/27794)) +- chore: remove old token details page ([#27774](https://github.com/MetaMask/metamask-extension/pull/27774)) +- chore: remove token list display component ([#27772](https://github.com/MetaMask/metamask-extension/pull/27772)) +- chore: update Trezor Connect to v9.4.0, remove workarounds ([#27112](https://github.com/MetaMask/metamask-extension/pull/27112)) +- test: [POM] Migrate transaction with snap account e2e tests to page object modal ([#27760](https://github.com/MetaMask/metamask-extension/pull/27760)) +- fix(snaps): Restore confirmation switching on routed confirmation ([#27753](https://github.com/MetaMask/metamask-extension/pull/27753)) +- Merge origin/develop into master-sync +- test: Onboarding: Fix vault-decryption-chrome.spec.js ([#27779](https://github.com/MetaMask/metamask-extension/pull/27779)) +- feat: support gas fee flows in standard swaps ([#27612](https://github.com/MetaMask/metamask-extension/pull/27612)) +- feat: Token send heading component ([#27562](https://github.com/MetaMask/metamask-extension/pull/27562)) +- feat: adds the new default settings view to onboarding ([#24562](https://github.com/MetaMask/metamask-extension/pull/24562)) +- chore(3212): remove alert settings ([#27709](https://github.com/MetaMask/metamask-extension/pull/27709)) +- docs: remove outdated Medium link, update "Twitter" to "X" ([#26692](https://github.com/MetaMask/metamask-extension/pull/26692)) +- fix: Replace 'transaction fees' with 'network fees' in the insufficie… ([#27762](https://github.com/MetaMask/metamask-extension/pull/27762)) +- fix: issue with Snap title in Snap Authorship Header ([#27752](https://github.com/MetaMask/metamask-extension/pull/27752)) +- fix: SIWE signature page displays parsed URI instead of domain ([#27754](https://github.com/MetaMask/metamask-extension/pull/27754)) +- fix: updated toasts component and copy ([#27656](https://github.com/MetaMask/metamask-extension/pull/27656)) +- feat: add network picker to AssetPicker ([#26559](https://github.com/MetaMask/metamask-extension/pull/26559)) +- fix(btc): fix jazzicons generations ([#27662](https://github.com/MetaMask/metamask-extension/pull/27662)) +- feat: Release Chain Permissions ([#27561](https://github.com/MetaMask/metamask-extension/pull/27561)) +- feat: upgrade assets-controllers to v38.2.0 ([#27629](https://github.com/MetaMask/metamask-extension/pull/27629)) +- ci: followup to CircleCI Sentry reporting ([#27548](https://github.com/MetaMask/metamask-extension/pull/27548)) +- chore: Master sync ([#27729](https://github.com/MetaMask/metamask-extension/pull/27729)) +- fix(multichain): fix getMultichainCurrentCurrency selector ([#27726](https://github.com/MetaMask/metamask-extension/pull/27726)) +- fix: Limit amount of decimals on spending cap modal ([#27672](https://github.com/MetaMask/metamask-extension/pull/27672)) +- Merge origin/develop into master-sync +- test: [POM] Migrate create snap account e2e tests to page object modal ([#27697](https://github.com/MetaMask/metamask-extension/pull/27697)) +- fix: Prefer token symbol to token name ([#27693](https://github.com/MetaMask/metamask-extension/pull/27693)) +- fix(btc): fetch btc balance right after account creation ([#27628](https://github.com/MetaMask/metamask-extension/pull/27628)) +- fix: UI startup with no Sentry DSN ([#27714](https://github.com/MetaMask/metamask-extension/pull/27714)) +- feat: Sort/Import Tokens in Extension ([#27184](https://github.com/MetaMask/metamask-extension/pull/27184)) +- ci: make git-diff-develop work for PRs from foreign repos ([#27268](https://github.com/MetaMask/metamask-extension/pull/27268)) +- test: Convert json-rpc e2e tests to TypeScript ([#27659](https://github.com/MetaMask/metamask-extension/pull/27659)) +- fix: allow getAddTransactionRequest to pass through other params ([#27117](https://github.com/MetaMask/metamask-extension/pull/27117)) +- perf: add tags to UI startup trace ([#27550](https://github.com/MetaMask/metamask-extension/pull/27550)) +- fix: Disable redirecting Extension users using beta & flask build and dev env to the existing offboarding page ([#27226](https://github.com/MetaMask/metamask-extension/pull/27226)) +- feat(NOTIFY-1193): add profile sync dev menu ([#27666](https://github.com/MetaMask/metamask-extension/pull/27666)) +- refactor: Typescript conversion of log-web3-shim-usage.js ([#23732](https://github.com/MetaMask/metamask-extension/pull/23732)) +- test: removing race condition for asserting inner values (PR-#2) ([#27664](https://github.com/MetaMask/metamask-extension/pull/27664)) +- fix(btc): fix address validation ([#27690](https://github.com/MetaMask/metamask-extension/pull/27690)) +- chore: Update coverage.json ([#27696](https://github.com/MetaMask/metamask-extension/pull/27696)) +- fix: test coverage quality gate ([#27691](https://github.com/MetaMask/metamask-extension/pull/27691)) +- fix: banner alert to render multiple general alerts ([#27339](https://github.com/MetaMask/metamask-extension/pull/27339)) +- refactor: routes constants ([#27078](https://github.com/MetaMask/metamask-extension/pull/27078)) +- fix: Test coverage quality gate ([#27581](https://github.com/MetaMask/metamask-extension/pull/27581)) +- feat: Adding delete metametrics data to security and privacy tab ([#24571](https://github.com/MetaMask/metamask-extension/pull/24571)) +- feat(stx): animations and cosmetic changes to smart transaction status page ([#27650](https://github.com/MetaMask/metamask-extension/pull/27650)) +- build: add lottie-web dependency to extension ([#27632](https://github.com/MetaMask/metamask-extension/pull/27632)) +- fix(btc): do not show percentage for tokens ([#27637](https://github.com/MetaMask/metamask-extension/pull/27637)) +- feat: support Etherscan API keys ([#27611](https://github.com/MetaMask/metamask-extension/pull/27611)) +- feat: change survey timeout time from a week to a day ([#27603](https://github.com/MetaMask/metamask-extension/pull/27603)) +- fix: Design papercuts for redesigned transactions ([#27605](https://github.com/MetaMask/metamask-extension/pull/27605)) +- test: removing race condition for asserting inner values (PR-#1) ([#27606](https://github.com/MetaMask/metamask-extension/pull/27606)) +- test: [POM] Migrate Snap Simple Keyring page and Snap List page to page object modal ([#27327](https://github.com/MetaMask/metamask-extension/pull/27327)) +- fix: fix sentry reading undefined ([#27584](https://github.com/MetaMask/metamask-extension/pull/27584)) +- fix: fix sentry reading null ([#27582](https://github.com/MetaMask/metamask-extension/pull/27582)) +- fix(btc): disable balanceIsCached flag ([#27636](https://github.com/MetaMask/metamask-extension/pull/27636)) +- chore: update accounts related packages ([#27284](https://github.com/MetaMask/metamask-extension/pull/27284)) +- chore: set bridge src network, tokens and top assets ([#26214](https://github.com/MetaMask/metamask-extension/pull/26214)) +- test: [Snaps E2E] add delay to installed snaps test to reduce flaking ([#27521](https://github.com/MetaMask/metamask-extension/pull/27521)) +- chore: set bridge dest network, tokens and top assets ([#26213](https://github.com/MetaMask/metamask-extension/pull/26213)) +- fix: fix reading address from market data ([#27604](https://github.com/MetaMask/metamask-extension/pull/27604)) +- feat: Migrate AccountTrackerController to BaseController v2 ([#27258](https://github.com/MetaMask/metamask-extension/pull/27258)) +- fix: disable transaction data decode if deployment ([#27586](https://github.com/MetaMask/metamask-extension/pull/27586)) +- fix: revert jest collect coverage patterns ([#27583](https://github.com/MetaMask/metamask-extension/pull/27583)) +- fix: add amount row for contract deployment ([#27594](https://github.com/MetaMask/metamask-extension/pull/27594)) +- fix: "Dapp viewed Event @no-mmi is sent when refreshing da..." flaky test ([#27381](https://github.com/MetaMask/metamask-extension/pull/27381)) +- chore: fix deps audit ([#27620](https://github.com/MetaMask/metamask-extension/pull/27620)) +- fix: Max approval and array value spending cap bugs ([#27573](https://github.com/MetaMask/metamask-extension/pull/27573)) +- feat: add power users survey support ([#27361](https://github.com/MetaMask/metamask-extension/pull/27361)) +- fix: Recreate offscreen document if it already exists ([#27596](https://github.com/MetaMask/metamask-extension/pull/27596)) +- fix: flaky test `Block Explorer links to the token tracker in the explorer` ([#27599](https://github.com/MetaMask/metamask-extension/pull/27599)) +- fix(snaps): `Copyable` more button color ([#27600](https://github.com/MetaMask/metamask-extension/pull/27600)) +- fix: flaky test `Import flow allows importing multiple tokens from search` ([#27567](https://github.com/MetaMask/metamask-extension/pull/27567)) +- fix(27428): fix if we type enter anything followed by a \ in settings search ([#27432](https://github.com/MetaMask/metamask-extension/pull/27432)) +- fix: flaky test `Address Book Edit entry in address book` due to race condition with mmi menu ([#27557](https://github.com/MetaMask/metamask-extension/pull/27557)) +- refactor: Typescript conversion of get-provider-state.js ([#23635](https://github.com/MetaMask/metamask-extension/pull/23635)) +- chore: Use "gas_included" event prop ([#27559](https://github.com/MetaMask/metamask-extension/pull/27559)) +- fix: mock locale in unit test ([#27574](https://github.com/MetaMask/metamask-extension/pull/27574)) +- feat: codefence Account Watcher for flask ([#27543](https://github.com/MetaMask/metamask-extension/pull/27543)) +- chore: start upgrade to React Router v6 ([#27185](https://github.com/MetaMask/metamask-extension/pull/27185)) +- fix: AmonHenV2 connection flow incremental permitted chain approval and account address case comparison ([#27518](https://github.com/MetaMask/metamask-extension/pull/27518)) +- fix: flaky test `Backup and Restore should backup the account settings` ([#27565](https://github.com/MetaMask/metamask-extension/pull/27565)) +- fix: Apply flex to Snaps buttons only when containing images and icons ([#27564](https://github.com/MetaMask/metamask-extension/pull/27564)) +- feat: aggregated balance feature ([#27097](https://github.com/MetaMask/metamask-extension/pull/27097)) +- feat: Add redesign integration tests ([#27259](https://github.com/MetaMask/metamask-extension/pull/27259)) +- fix: flaky test `4byte setting does not try to get contract method name from 4byte when the setting is off` ([#27560](https://github.com/MetaMask/metamask-extension/pull/27560)) +- feat: add merge queue ([#26871](https://github.com/MetaMask/metamask-extension/pull/26871)) +- feat: remove squiggle animation from swaps smart transactions ([#27264](https://github.com/MetaMask/metamask-extension/pull/27264)) +- feat: Enable gas included swaps ([#27427](https://github.com/MetaMask/metamask-extension/pull/27427)) +- fix(snaps): Fix custom UI buttons submitting forms ([#27531](https://github.com/MetaMask/metamask-extension/pull/27531)) +- chore: Master sync following v12.3.1 ([#27538](https://github.com/MetaMask/metamask-extension/pull/27538)) +- Merge origin/develop into master-sync +- fix(NOTIFY-1171): account syncing performance and bug fixes ([#27529](https://github.com/MetaMask/metamask-extension/pull/27529)) +- fix: genUnapprovedApproveConfirmation import path ([#27530](https://github.com/MetaMask/metamask-extension/pull/27530)) +- fix(snaps): Keep focus on input if interface re-renders ([#27429](https://github.com/MetaMask/metamask-extension/pull/27429)) +- fix: Allow state updates in Snaps interfaces to state values that are falsy ([#27488](https://github.com/MetaMask/metamask-extension/pull/27488)) +- fix: updated ui for connect and review page ([#27478](https://github.com/MetaMask/metamask-extension/pull/27478)) +- feat: Custom header for wallet initiated confirmations ([#27391](https://github.com/MetaMask/metamask-extension/pull/27391)) +- feat: convert account tracker to typescript ([#27231](https://github.com/MetaMask/metamask-extension/pull/27231)) +- fix: Fix snaps permission connection for `CHAIN_PERMISSIONS` feature flag ([#27459](https://github.com/MetaMask/metamask-extension/pull/27459)) +- fix: flaky test `Navigation Signature - Different signature types initiates multiple signatures and rejects all` ([#27481](https://github.com/MetaMask/metamask-extension/pull/27481)) +- feat: Double Sentry performance trace sample rate ([#27468](https://github.com/MetaMask/metamask-extension/pull/27468)) +- ci: Expand github bot policy update comment to be more actionable ([#27242](https://github.com/MetaMask/metamask-extension/pull/27242)) +- chore: Add `useLedgerConnection` unit tests ([#27358](https://github.com/MetaMask/metamask-extension/pull/27358)) +- ci: Sentry reporting only on develop branch, with Git message overrides ([#27412](https://github.com/MetaMask/metamask-extension/pull/27412)) +- test: Fix flaky permit test ([#27450](https://github.com/MetaMask/metamask-extension/pull/27450)) +- fix: removed closeMenu for ConnectedAccountsMenu ([#27460](https://github.com/MetaMask/metamask-extension/pull/27460)) +- fix(snaps): Set proper text color for secondary button ([#27335](https://github.com/MetaMask/metamask-extension/pull/27335)) +- chore: set bridge selected tokens and amount ([#26212](https://github.com/MetaMask/metamask-extension/pull/26212)) +- fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi`aded ([#27420](https://github.com/MetaMask/metamask-extension/pull/27420)) +- fix: flaky test `Responsive UI Send Transaction from responsive window` ([#27417](https://github.com/MetaMask/metamask-extension/pull/27417)) +- fix: flaky test `Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx should queue send tx after switch network confirmation and transaction should target the correct network after switch is confirmed` ([#27352](https://github.com/MetaMask/metamask-extension/pull/27352)) +- fix: Change speed key color ([#27416](https://github.com/MetaMask/metamask-extension/pull/27416)) +- feat: Display setApprovalForAll and revoke setApprovalForAll to users… ([#27401](https://github.com/MetaMask/metamask-extension/pull/27401)) +- fix: "Warning: Invalid argument supplied to oneOfType" ([#27267](https://github.com/MetaMask/metamask-extension/pull/27267)) +- feat: Editing flow ([#26635](https://github.com/MetaMask/metamask-extension/pull/26635)) +- chore: bump profile-sync-controller to 0.9.3 ([#27415](https://github.com/MetaMask/metamask-extension/pull/27415)) +- fix: Remove duplication ([#27421](https://github.com/MetaMask/metamask-extension/pull/27421)) +- fix: Confirm Page test failing in CI/CD ([#27423](https://github.com/MetaMask/metamask-extension/pull/27423)) +- feat: Display approve, increaseAllowance and revoke approval to users… ([#26985](https://github.com/MetaMask/metamask-extension/pull/26985)) +- feat: Add performance metrics for signature requests ([#26967](https://github.com/MetaMask/metamask-extension/pull/26967)) +- fix: Permit DataTree token decimals ([#27328](https://github.com/MetaMask/metamask-extension/pull/27328)) +- fix: alert system and refine SIWE and contract interaction alerts ([#27205](https://github.com/MetaMask/metamask-extension/pull/27205)) +- fix(NOTIFY-1166): rename account sync event names ([#27413](https://github.com/MetaMask/metamask-extension/pull/27413)) +- feat: ERC20 Revoke Allowance ([#26906](https://github.com/MetaMask/metamask-extension/pull/26906)) + ## [12.5.0] ### Added - New UI and functionality for adding and managing networks ([#26433](https://github.com/MetaMask/metamask-extension/pull/26433)), ([#27085](https://github.com/MetaMask/metamask-extension/pull/27085)) @@ -5223,7 +5436,8 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.5.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.6.0...HEAD +[12.6.0]: https://github.com/MetaMask/metamask-extension/compare/v12.5.0...v12.6.0 [12.5.0]: https://github.com/MetaMask/metamask-extension/compare/v12.4.2...v12.5.0 [12.4.2]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...v12.4.2 [12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1 diff --git a/README.md b/README.md index d70eb03a32b2..f3e738a40abc 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/). @@ -84,12 +84,15 @@ If you are using VS Code and are unable to make commits from the source control To start a development build (e.g. with logging and file watching) run `yarn start`. #### Development build with wallet state + You can start a development build with a preloaded wallet state, by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file. Then you have the following options: + 1. Start the wallet with the default fixture flags, by running `yarn start:with-state`. 2. Check the list of available fixture flags, by running `yarn start:with-state --help`. 3. Start the wallet with custom fixture flags, by running `yarn start:with-state --FIXTURE_NAME=VALUE` for example `yarn start:with-state --withAccounts=100`. You can pass as many flags as you want. The rest of the fixtures will take the default values. #### Development build with Webpack + You can also start a development build using the `yarn webpack` command, or `yarn webpack --watch`. This uses an alternative build system that is much faster, but not yet production ready. See the [Webpack README](./development/webpack/README.md) for more information. #### React and Redux DevTools @@ -191,7 +194,7 @@ Different build types have different e2e tests sets. In order to run them look i ```console "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:snaps": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --snaps", - "test:e2e:firefox": "ENABLE_MV3=false SELENIUM_BROWSER=firefox node test/e2e/run-all.js", + "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", ``` #### Note: Running MMI e2e tests 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..fe0c84afcfac 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Sensible Informationen verbergen" }, - "hideToken": { - "message": "Token verbergen" - }, "hideTokenPrompt": { "message": "Token verbergen?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON Datei", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Keine Konten für die angegebene Suchanfrage gefunden" }, - "noConnectedAccountDescription": { - "message": "Wählen Sie ein Konto, das Sie auf dieser Website verwenden möchten, um fortzufahren." - }, "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, - "noConversionDateAvailable": { - "message": "Kein Umrechnungskursdaten verfügbar" - }, "noConversionRateAvailable": { "message": "Kein Umrechnungskurs verfügbar" }, @@ -3501,10 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Abgelehnt" }, - "remember": { - "message": "Erinnern:" - }, "remove": { "message": "Entfernen" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Wir sind bereit, Ihnen die neuesten Angebote zu zeigen, wenn Sie fortfahren möchten." }, - "swapBuildQuotePlaceHolderText": { - "message": "Keine Tokens verfügbar, die mit $1 übereinstimmen.", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Mit Ihrer Hardware-Wallet bestätigen" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Fehler beim Abrufen der Preisangaben" }, - "swapFetchingTokens": { - "message": "Token abrufen..." - }, "swapFromTo": { "message": "Swap von $1 auf $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Hohe Slippage" }, - "swapHighSlippageWarning": { - "message": "Der Slippage-Betrag ist sehr hoch." - }, "swapIncludesMMFee": { "message": "Enthält eine MetaMask-Gebühr von $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Niedrige Slippage" }, - "swapLowSlippageError": { - "message": "Transaktion kann fehlschlagen, maximale Slippage zu niedrig." - }, "swapMaxSlippage": { "message": "Max. Slippage" }, @@ -5438,9 +5331,6 @@ "message": "Preisdifferenz von ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Der Preiseinfluss ist die Differenz zwischen dem aktuellen Marktpreis und dem bei der Ausführung der Transaktion erhaltenen Betrag. Die Preisauswirkung ist eine Funktion der Größe Ihres Geschäfts im Verhältnis zur Größe des Liquiditätspools." - }, "swapPriceUnavailableDescription": { "message": "Die Auswirkungen auf den Preis konnten aufgrund fehlender Marktpreisdaten nicht ermittelt werden. Bitte bestätigen Sie vor dem Tausch, dass Sie mit der Menge der Tokens, die Sie erhalten werden, einverstanden sind." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Angebotsanfrage" }, - "swapReviewSwap": { - "message": "Swap überprüfen" - }, - "swapSearchNameOrAddress": { - "message": "Namen suchen oder Adresse einfügen" - }, "swapSelect": { "message": "Auswählen" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Niedriger Slippage" }, - "swapSlippageNegative": { - "message": "Slippage muss größer oder gleich Null sein" - }, "swapSlippageNegativeDescription": { "message": "Slippage muss größer oder gleich Null sein" }, @@ -5596,20 +5477,6 @@ "message": "$1 mit $2 tauschen", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Dieses Token wurde manuell hinzugefügt." - }, - "swapTokenVerificationMessage": { - "message": "Bestätigen Sie immer die Token-Adresse auf $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Nur an 1 Quelle verifiziert." - }, - "swapTokenVerificationSources": { - "message": "Auf $1 Quellen überprüft.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 wurde nur auf 1 Quelle bestätigt. Ziehen Sie in Betracht, es vor dem Fortfahren auf $2 zu bestätigen.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Unbekannt" }, - "swapVerifyTokenExplanation": { - "message": "Mehrere Token können denselben Namen und dasselbe Symbol verwenden. Überprüfen Sie $1, um sicherzugehen, dass dies der Token ist, den Sie suchen.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 zum Swap verfügbar", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % Slippage" }, - "swapsAdvancedOptions": { - "message": "Erweiterte Optionen" - }, - "swapsExcessiveSlippageWarning": { - "message": "Der Slippage-Betrag ist zu hoch und wird zu einem schlechten Kurs führen. Bitte reduzieren Sie die Slippage-Toleranz auf einen Wert unter 15 %." - }, "swapsMaxSlippage": { "message": "Slippage-Toleranz" }, - "swapsNotEnoughForTx": { - "message": "Nicht genug $1, um diese Transaktion abzuschließen.", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Nicht genügend $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Tokendezimal erforderlich. Finden Sie es auf: $1" }, - "tokenDecimalTitle": { - "message": "Token-Dezimale:" - }, "tokenDetails": { "message": "Token-Details" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Aktualisierungsanfrage" }, - "updatedWithDate": { - "message": "$1 aktualisiert" - }, "uploadDropFile": { "message": "Legen Sie Ihre Datei hier ab" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Drittanbieter-Details überprüfen" }, - "verifyThisTokenOn": { - "message": "Diesen Token auf $1 verifizieren", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Überprüfen Sie diesen Token auf $1 und stellen Sie sicher, dass dies der Token ist, den Sie handeln möchten.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, @@ -6229,26 +6064,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..5c42a8d829b4 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Απόκρυψη ευαίσθητων πληροφοριών" }, - "hideToken": { - "message": "Απόκρυψη token" - }, "hideTokenPrompt": { "message": "Απόκρυψη του token;" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Αρχείο JSON", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Δεν βρέθηκαν λογαριασμοί για το συγκεκριμένο αίτημα αναζήτησης" }, - "noConnectedAccountDescription": { - "message": "Επιλέξτε έναν λογαριασμό που θέλετε να χρησιμοποιήσετε σε αυτόν τον ιστότοπο για να συνεχίσετε." - }, "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, - "noConversionDateAvailable": { - "message": "Δεν υπάρχει διαθέσιμη ημερομηνία μετατροπής νομίσματος" - }, "noConversionRateAvailable": { "message": "Δεν υπάρχει διαθέσιμη ισοτιμία μετατροπής" }, @@ -3501,10 +3434,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 +3850,6 @@ "priceUnavailable": { "message": "μη διαθέσιμη τιμή" }, - "primaryCurrencySetting": { - "message": "Κύριο νόμισμα" - }, - "primaryCurrencySettingDescription": { - "message": "Επιλέξτε εγχώριο για να δώσετε προτεραιότητα στην εμφάνιση των τιμών στο νόμισμα της αλυσίδας (π.χ. ETH). Επιλέξτε Παραστατικό για να δώσετε προτεραιότητα στην εμφάνιση τιμών στο επιλεγμένο παραστατικό νόμισμα." - }, "primaryType": { "message": "Βασικός τύπος" }, @@ -4151,9 +4074,6 @@ "rejected": { "message": "Απορρίφθηκε" }, - "remember": { - "message": "Να θυμάστε:" - }, "remove": { "message": "Κατάργηση" }, @@ -4561,9 +4481,6 @@ "sepolia": { "message": "Δίκτυο δοκιμών Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "Το MetaMask χρησιμοποιεί αυτές τις αξιόπιστες υπηρεσίες τρίτων για να ενισχύσει τη χρηστικότητα και την ασφάλεια των προϊόντων." - }, "setApprovalForAll": { "message": "Ρύθμιση έγκρισης για όλους" }, @@ -4753,13 +4670,6 @@ "smartTransactionSuccess": { "message": "Η συναλλαγή σας ολοκληρώθηκε" }, - "smartTransactionTakingTooLong": { - "message": "Συγγνώμη για την αναμονή" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Εάν η συναλλαγή σας δεν ολοκληρωθεί εντός $1, θα ακυρωθεί και δεν θα χρεωθείτε με τέλη συναλλαγών.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Έξυπνες συναλλαγές" }, @@ -5176,10 +5086,6 @@ "stxCancelledSubDescription": { "message": "Προσπαθήστε ξανά να κάνετε ανταλλαγή. Θα είμαστε εδώ για να σας προστατεύσουμε από παρόμοιους κινδύνους και την επόμενη φορά." }, - "stxEstimatedCompletion": { - "message": "Εκτιμώμενη ολοκλήρωση σε < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Η ανταλλαγή απέτυχε" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Είμαστε έτοιμοι να σας δείξουμε τις τελευταίες προσφορές, όποτε θέλετε να συνεχίσετε" }, - "swapBuildQuotePlaceHolderText": { - "message": "Δεν υπάρχουν διαθέσιμα tokens που να αντιστοιχούν σε $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Επιβεβαιώστε με το πορτοφόλι υλικού σας" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Σφάλμα κατά τη λήψη προσφορών" }, - "swapFetchingTokens": { - "message": "Λήψη tokens..." - }, "swapFromTo": { "message": "Η ανταλλαγή από $1 έως $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Υψηλή ολίσθηση" }, - "swapHighSlippageWarning": { - "message": "Το ποσό ολίσθησης είναι πολύ υψηλό." - }, "swapIncludesMMFee": { "message": "Περιλαμβάνει μια χρέωση $1% στο MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Χαμηλή ολίσθηση" }, - "swapLowSlippageError": { - "message": "Η συναλλαγή ενδέχεται να αποτύχει, η μέγιστη ολίσθηση είναι πολύ χαμηλή." - }, "swapMaxSlippage": { "message": "Μέγιστη ολίσθηση" }, @@ -5438,9 +5331,6 @@ "message": "Διαφορά τιμής ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Η επίπτωση στις τιμές είναι η διαφορά μεταξύ της τρέχουσας τιμής αγοράς και του ποσού που εισπράττεται κατά την εκτέλεση της συναλλαγής. Η επίπτωση στις τιμές είναι συνάρτηση του μεγέθους της συναλλαγής σας σε σχέση με το μέγεθος του αποθέματος ρευστότητας." - }, "swapPriceUnavailableDescription": { "message": "Η επίπτωση στις τιμές δεν ήταν δυνατόν να προσδιοριστεί λόγω έλλειψης στοιχείων για τις τιμές της αγοράς. Παρακαλούμε επιβεβαιώστε ότι είστε ικανοποιημένοι με το ποσό των tokens που πρόκειται να λάβετε πριν από την ανταλλαγή." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Αίτημα για προσφορά" }, - "swapReviewSwap": { - "message": "Έλεγχος της ανταλλαγής" - }, - "swapSearchNameOrAddress": { - "message": "Αναζήτηση ονόματος ή επικόλληση διεύθυνσης" - }, "swapSelect": { "message": "Επιλογή" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Χαμηλή απόκλιση" }, - "swapSlippageNegative": { - "message": "Η απόκλιση πρέπει να είναι μεγαλύτερη ή ίση με το μηδέν" - }, "swapSlippageNegativeDescription": { "message": "Η απόκλιση πρέπει να είναι μεγαλύτερη ή ίση με μηδέν" }, @@ -5596,20 +5477,6 @@ "message": "Ανταλλαγή $1 έως $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Αυτό το token έχει προστεθεί χειροκίνητα." - }, - "swapTokenVerificationMessage": { - "message": "Πάντα να επιβεβαιώνετε τη διεύθυνση του token στο $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Επαληθεύτηκε μόνο σε 1 πηγή." - }, - "swapTokenVerificationSources": { - "message": "Επαληθεύτηκε μόνο σε $1 πηγές.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "Το $1 επαληθεύτηκε μόνο από 1 πηγή. Εξετάστε το ενδεχόμενο επαλήθευσης σε $2 πριν προχωρήσετε.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Άγνωστο" }, - "swapVerifyTokenExplanation": { - "message": "Πολλαπλά tokens μπορούν να χρησιμοποιούν το ίδιο όνομα και σύμβολο. Ελέγξτε το $1 για να επιβεβαιώσετε ότι αυτό είναι το token που ψάχνετε.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 διαθέσιμα για ανταλλαγή", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Απόκλιση" }, - "swapsAdvancedOptions": { - "message": "Προηγμένες επιλογές" - }, - "swapsExcessiveSlippageWarning": { - "message": "Το ποσοστό απόκλισης είναι πολύ υψηλό και θα οδηγήσει σε χαμηλή τιμή. Παρακαλούμε μειώστε την ανοχή απόκλισης σε τιμή κάτω του 15%." - }, "swapsMaxSlippage": { "message": "Ανοχή απόκλισης" }, - "swapsNotEnoughForTx": { - "message": "Δεν υπάρχουν αρκετά $1 για να ολοκληρωθεί αυτή η συναλλαγή", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Δεν υπάρχουν αρκετά $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Απαιτείται δεκαδικό token. Βρείτε το σε: $1" }, - "tokenDecimalTitle": { - "message": "Δεκαδικά Ψηφία του token:" - }, "tokenDetails": { "message": "Λεπτομέρειες του token" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Αίτημα ενημέρωσης" }, - "updatedWithDate": { - "message": "Ενημερώθηκε $1" - }, "uploadDropFile": { "message": "Αφήστε το αρχείο σας εδώ" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Επαλήθευση στοιχείων τρίτων" }, - "verifyThisTokenOn": { - "message": "Επαλήθευση αυτού του token στο $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Επαληθεύστε αυτό το token στο $1 και βεβαιωθείτε ότι αυτό είναι το token που θέλετε να κάνετε συναλλαγές.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Έκδοση" }, @@ -6229,26 +6064,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..70cb4da8cfb6 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,15 @@ "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeFrom": { + "message": "Bridge from" + }, + "bridgeSelectNetwork": { + "message": "Select network" + }, + "bridgeTo": { + "message": "Bridge to" + }, "browserNotSupported": { "message": "Your browser is not supported..." }, @@ -1014,12 +1015,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 +1063,9 @@ "confirmTitlePermitTokens": { "message": "Spending cap request" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Remove permission" + }, "confirmTitleSIWESignature": { "message": "Sign-in request" }, @@ -1080,6 +1078,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 +1096,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 +1176,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 +1215,9 @@ "connectingToSepolia": { "message": "Connecting to Sepolia test network" }, + "connectionDescription": { + "message": "This site wants to" + }, "connectionFailed": { "message": "Connection failed" }, @@ -1319,7 +1338,7 @@ "message": "CryptoCompare" }, "currencyConversion": { - "message": "Currency conversion" + "message": "Currency" }, "currencyRateCheckToggle": { "message": "Show balance and token price checker" @@ -1531,12 +1550,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 privacy 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 +1639,14 @@ "disconnectAllAccountsText": { "message": "accounts" }, + "disconnectAllDescriptionText": { + "message": "If you disconnect from this site, you’ll need to reconnect your accounts and networks to use this site again." + }, "disconnectAllSnapsText": { "message": "Snaps" }, - "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 this site" }, "disconnectPrompt": { "message": "Disconnect $1" @@ -1766,6 +1812,9 @@ "editPermission": { "message": "Edit permission" }, + "editPermissions": { + "message": "Edit permissions" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Edit speed up gas fee" }, @@ -1778,6 +1827,9 @@ "editSpendingCapDesc": { "message": "Enter the amount that you feel comfortable being spent on your behalf." }, + "editSpendingCapError": { + "message": "The spending cap can’t exceed $1 decimal digits. Remove decimal digits to continue." + }, "enable": { "message": "Enable" }, @@ -1993,10 +2045,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 +2130,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 +2203,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" }, @@ -2234,9 +2288,6 @@ "hideSentitiveInfo": { "message": "Hide sensitive information" }, - "hideToken": { - "message": "Hide token" - }, "hideTokenPrompt": { "message": "Hide token?" }, @@ -2315,6 +2366,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 +2451,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 +2620,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 +2686,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 +2696,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 +2850,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 privacy settings" + }, "marketCap": { "message": "Market cap" }, @@ -2828,6 +2896,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 +2975,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 +3056,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 +3161,9 @@ "networkOptions": { "message": "Network options" }, + "networkPermissionToast": { + "message": "Network permissions updated" + }, "networkProvider": { "message": "Network provider" }, @@ -3124,6 +3202,9 @@ "networks": { "message": "Networks" }, + "networksSmallCase": { + "message": "networks" + }, "nevermind": { "message": "Nevermind" }, @@ -3236,14 +3317,11 @@ "noAccountsFound": { "message": "No accounts found for the given search query" }, - "noConnectedAccountDescription": { - "message": "Select an account you want to use on this site to continue." - }, "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" + "noConnectionDescription": { + "message": "To connect to a site, find and select the \"connect\" button. Remember MetaMask can only connect to sites on web3" }, "noConversionRateAvailable": { "message": "No conversion rate available" @@ -3648,10 +3726,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 +4105,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 +4159,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 +4389,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 +4474,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 +4569,12 @@ "revealTheSeedPhrase": { "message": "Reveal seed phrase" }, + "review": { + "message": "Review" + }, + "reviewAlert": { + "message": "Review alert" + }, "reviewAlerts": { "message": "Review alerts" }, @@ -4513,6 +4600,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 +4665,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 +4837,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 +4853,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 +4902,9 @@ "showMore": { "message": "Show more" }, + "showNativeTokenAsMainBalance": { + "message": "Show native token as main balance" + }, "showNft": { "message": "Show NFT" }, @@ -4946,18 +5045,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 +5251,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 +5485,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" }, @@ -5499,10 +5597,6 @@ "swapAreYouStillThereDescription": { "message": "We’re ready to show you the latest quotes when you want to continue" }, - "swapBuildQuotePlaceHolderText": { - "message": "No tokens available matching $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -5567,9 +5661,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error fetching quotes" }, - "swapFetchingTokens": { - "message": "Fetching tokens..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5595,11 +5686,18 @@ "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.", @@ -5622,9 +5720,6 @@ "swapLowSlippage": { "message": "Low slippage" }, - "swapLowSlippageError": { - "message": "Transaction may fail, max slippage too low." - }, "swapMaxSlippage": { "message": "Max slippage" }, @@ -5659,9 +5754,6 @@ "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, @@ -5708,12 +5800,6 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, - "swapReviewSwap": { - "message": "Review swap" - }, - "swapSearchNameOrAddress": { - "message": "Search name or paste address" - }, "swapSelect": { "message": "Select" }, @@ -5746,9 +5832,6 @@ "swapSlippageLowTitle": { "message": "Low slippage" }, - "swapSlippageNegative": { - "message": "Slippage must be greater or equal to zero" - }, "swapSlippageNegativeDescription": { "message": "Slippage must be greater or equal to zero" }, @@ -5817,20 +5900,6 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "This token has been added manually." - }, - "swapTokenVerificationMessage": { - "message": "Always confirm the token address on $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Only verified on 1 source." - }, - "swapTokenVerificationSources": { - "message": "Verified on $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 is only verified on 1 source. Consider verifying it on $2 before proceeding.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5851,30 +5920,12 @@ "swapUnknown": { "message": "Unknown" }, - "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 available to swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Advanced options" - }, - "swapsExcessiveSlippageWarning": { - "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Not enough $1 to complete this transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Not enough $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6013,9 +6064,6 @@ "tokenDecimalFetchFailed": { "message": "Token decimal required. Find it on: $1" }, - "tokenDecimalTitle": { - "message": "Token decimal:" - }, "tokenDetails": { "message": "Token details" }, @@ -6131,6 +6179,9 @@ "transactionFee": { "message": "Transaction fee" }, + "transactionFlowNetwork": { + "message": "Network" + }, "transactionHistoryBaseFee": { "message": "Base fee (GWEI)" }, @@ -6179,6 +6230,9 @@ "transferFrom": { "message": "Transfer from" }, + "transferRequest": { + "message": "Transfer request" + }, "trillionAbbreviation": { "message": "T", "description": "Shortened form of 'trillion'" @@ -6331,9 +6385,6 @@ "updatedRpcForNetworks": { "message": "Network RPCs Updated" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, @@ -6395,14 +6446,6 @@ "verifyContractDetails": { "message": "Verify third-party details" }, - "verifyThisTokenOn": { - "message": "Verify this token on $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verify this token on $1 and make sure this is the token you want to trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, @@ -6469,25 +6512,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 +6617,9 @@ "yourBalance": { "message": "Your balance" }, + "yourBalanceIsAggregated": { + "message": "Your balance is aggregated" + }, "yourNFTmayBeAtRisk": { "message": "Your NFT may be at risk" }, @@ -6602,6 +6632,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..fc635e33a708 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" @@ -5381,10 +5341,6 @@ "swapAreYouStillThereDescription": { "message": "We’re ready to show you the latest quotes when you want to continue" }, - "swapBuildQuotePlaceHolderText": { - "message": "No tokens available matching $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -5449,9 +5405,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error fetching quotes" }, - "swapFetchingTokens": { - "message": "Fetching tokens..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5472,9 +5425,6 @@ "swapHighSlippage": { "message": "High slippage" }, - "swapHighSlippageWarning": { - "message": "Slippage amount is very high." - }, "swapIncludesMMFee": { "message": "Includes a $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5496,9 +5446,6 @@ "swapLowSlippage": { "message": "Low slippage" }, - "swapLowSlippageError": { - "message": "Transaction may fail, max slippage too low." - }, "swapMaxSlippage": { "message": "Max slippage" }, @@ -5533,9 +5480,6 @@ "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, @@ -5582,12 +5526,6 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, - "swapReviewSwap": { - "message": "Review swap" - }, - "swapSearchNameOrAddress": { - "message": "Search name or paste address" - }, "swapSelect": { "message": "Select" }, @@ -5620,9 +5558,6 @@ "swapSlippageLowTitle": { "message": "Low slippage" }, - "swapSlippageNegative": { - "message": "Slippage must be greater or equal to zero" - }, "swapSlippageNegativeDescription": { "message": "Slippage must be greater or equal to zero" }, @@ -5691,20 +5626,6 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "This token has been added manually." - }, - "swapTokenVerificationMessage": { - "message": "Always confirm the token address on $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Only verified on 1 source." - }, - "swapTokenVerificationSources": { - "message": "Verified on $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 is only verified on 1 source. Consider verifying it on $2 before proceeding.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5725,30 +5646,12 @@ "swapUnknown": { "message": "Unknown" }, - "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 available to swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Advanced options" - }, - "swapsExcessiveSlippageWarning": { - "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Not enough $1 to complete this transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Not enough $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6231,9 +6134,6 @@ "updateRequest": { "message": "Update request" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, @@ -6298,14 +6198,6 @@ "verifyContractDetails": { "message": "Verify third-party details" }, - "verifyThisTokenOn": { - "message": "Verify this token on $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verify this token on $1 and make sure this is the token you want to trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 67143b9609d3..97d6f4be9854 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" @@ -2111,9 +2060,6 @@ "hideSentitiveInfo": { "message": "Ocultar información confidencial" }, - "hideToken": { - "message": "Ocultar token" - }, "hideTokenPrompt": { "message": "¿Ocultar token?" }, @@ -2439,9 +2385,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Archivo JSON", "description": "format for importing an account" @@ -2848,10 +2791,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" @@ -3089,15 +3028,9 @@ "noAccountsFound": { "message": "No se encuentran cuentas para la consulta de búsqueda determinada" }, - "noConnectedAccountDescription": { - "message": "Seleccione una cuenta que desee utilizar en este sitio para continuar." - }, "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -3498,10 +3431,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 +3847,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 +4071,6 @@ "rejected": { "message": "Rechazado" }, - "remember": { - "message": "Recuerde:" - }, "remove": { "message": "Quitar" }, @@ -4558,9 +4478,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 +4667,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 +5083,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" }, @@ -5283,10 +5189,6 @@ "swapAreYouStillThereDescription": { "message": "Estamos listos para mostrarle las últimas cotizaciones cuando desee continuar" }, - "swapBuildQuotePlaceHolderText": { - "message": "No hay tokens disponibles que coincidan con $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmar con su monedero físico" }, @@ -5351,9 +5253,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error al capturar cotizaciones" }, - "swapFetchingTokens": { - "message": "Capturando tokens…" - }, "swapFromTo": { "message": "El intercambio de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5374,9 +5273,6 @@ "swapHighSlippage": { "message": "Deslizamiento alto" }, - "swapHighSlippageWarning": { - "message": "El monto del deslizamiento es muy alto." - }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5398,9 +5294,6 @@ "swapLowSlippage": { "message": "Deslizamiento bajo" }, - "swapLowSlippageError": { - "message": "Es posible que la transacción tenga errores, el deslizamiento máximo es demasiado bajo." - }, "swapMaxSlippage": { "message": "Desfase máximo" }, @@ -5435,9 +5328,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." - }, "swapPriceUnavailableDescription": { "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el intercambio, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." }, @@ -5484,12 +5374,6 @@ "swapRequestForQuotation": { "message": "Solicitud de cotización" }, - "swapReviewSwap": { - "message": "Revisar intercambio" - }, - "swapSearchNameOrAddress": { - "message": "Buscar nombre o pegar dirección" - }, "swapSelect": { "message": "Seleccionar" }, @@ -5522,9 +5406,6 @@ "swapSlippageLowTitle": { "message": "Deslizamiento bajo" }, - "swapSlippageNegative": { - "message": "El deslizamiento debe ser mayor o igual que cero" - }, "swapSlippageNegativeDescription": { "message": "El deslizamiento debe ser mayor o igual que cero" }, @@ -5593,20 +5474,6 @@ "message": "Intercambiar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token se añadió de forma manual." - }, - "swapTokenVerificationMessage": { - "message": "Siempre confirme la dirección del token en $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Solo se verificó en una fuente." - }, - "swapTokenVerificationSources": { - "message": "Verificar en $1 fuentes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 solo se verifica en 1 fuente. Considere verificarlo en $2 antes de continuar.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5627,30 +5494,12 @@ "swapUnknown": { "message": "Desconocido" }, - "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibles para intercambio", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de deslizamiento" }, - "swapsAdvancedOptions": { - "message": "Opciones avanzadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "El monto del deslizamiento es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de deslizamiento a un valor menor al 15 %." - }, "swapsMaxSlippage": { "message": "Tolerancia de deslizamiento" }, - "swapsNotEnoughForTx": { - "message": "No hay $1 suficientes para completar esta transacción", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "No hay suficiente $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5786,9 +5635,6 @@ "tokenDecimalFetchFailed": { "message": "Se requiere decimal del token. Encuéntrelo en: $1" }, - "tokenDecimalTitle": { - "message": "Decimales del token:" - }, "tokenDetails": { "message": "Detalles del token" }, @@ -6088,9 +5934,6 @@ "updateRequest": { "message": "Solicitud de actualización" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "uploadDropFile": { "message": "Ingrese su archivo aquí" }, @@ -6152,14 +5995,6 @@ "verifyContractDetails": { "message": "Verificar detalles de terceros" }, - "verifyThisTokenOn": { - "message": "Comprobar este token en $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versión" }, @@ -6226,26 +6061,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..672823c370ba 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" }, @@ -1928,10 +1894,6 @@ "message": "Necesita $1 más $2 para realizar este canje", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "No hay tokens disponibles que coincidan con $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1983,9 +1945,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error al capturar cotizaciones" }, - "swapFetchingTokens": { - "message": "Capturando tokens..." - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -2003,16 +1962,10 @@ "message": "Las tarifas de gas se pagan a los mineros de criptomonedas que procesan transacciones en la red $1. MetaMask no se beneficia de las tarifas de gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, - "swapHighSlippageWarning": { - "message": "El monto del desfase es muy alto." - }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapLowSlippageError": { - "message": "Es posible que la transacción tenga errores, el desfase máximo es demasiado bajo." - }, "swapMaxSlippage": { "message": "Desfase máximo" }, @@ -2043,9 +1996,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." - }, "swapPriceUnavailableDescription": { "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el canje, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." }, @@ -2085,9 +2035,6 @@ "swapRequestForQuotation": { "message": "Solicitud de cotización" }, - "swapReviewSwap": { - "message": "Revisar canje" - }, "swapSelect": { "message": "Seleccionar" }, @@ -2100,9 +2047,6 @@ "swapSelectQuotePopoverDescription": { "message": "A continuación se muestran todas las cotizaciones recopiladas de diversas fuentes de liquidez." }, - "swapSlippageNegative": { - "message": "El desfase debe ser mayor o igual que cero" - }, "swapSource": { "message": "Fuente de liquidez" }, @@ -2136,20 +2080,6 @@ "message": "Canjear $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token se añadió de forma manual." - }, - "swapTokenVerificationMessage": { - "message": "Siempre confirme la dirección del token en $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Solo se verificó en una fuente." - }, - "swapTokenVerificationSources": { - "message": "Verificar en $1 fuentes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTooManyDecimalsError": { "message": "$1 permite hasta $2 decimales", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -2163,30 +2093,12 @@ "swapUnknown": { "message": "Desconocido" }, - "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponible para canje", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de desfase" }, - "swapsAdvancedOptions": { - "message": "Opciones avanzadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." - }, "swapsMaxSlippage": { "message": "Tolerancia de desfase" }, - "swapsNotEnoughForTx": { - "message": "No hay $1 suficientes para finalizar esta transacción", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Ver en actividad" }, @@ -2405,9 +2317,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." }, @@ -2426,14 +2335,6 @@ "userName": { "message": "Nombre de usuario" }, - "verifyThisTokenOn": { - "message": "Verificar este token en $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Ver todos los detalles" }, @@ -2470,26 +2371,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..dbaffd44cf38 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Masquer les informations sensibles" }, - "hideToken": { - "message": "Masquer le token" - }, "hideTokenPrompt": { "message": "Masquer le jeton ?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Fichier JSON", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Aucun compte trouvé pour la demande de recherche effectuée" }, - "noConnectedAccountDescription": { - "message": "Sélectionnez un compte que vous souhaitez utiliser sur ce site pour continuer." - }, "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, - "noConversionDateAvailable": { - "message": "Aucune date de conversion des devises n’est disponible" - }, "noConversionRateAvailable": { "message": "Aucun taux de conversion disponible" }, @@ -3501,10 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Rejeté" }, - "remember": { - "message": "Rappel :" - }, "remove": { "message": "Supprimer" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Si vous le souhaitez, nous sommes prêts à vous présenter les dernières cotations" }, - "swapBuildQuotePlaceHolderText": { - "message": "Aucun jeton disponible correspondant à $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmez avec votre portefeuille matériel" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erreur lors de la récupération des cotations" }, - "swapFetchingTokens": { - "message": "Récupération des jetons…" - }, "swapFromTo": { "message": "Le swap de $1 vers $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Important effet de glissement" }, - "swapHighSlippageWarning": { - "message": "Le montant du glissement est très élevé." - }, "swapIncludesMMFee": { "message": "Comprend des frais MetaMask à hauteur de $1 %.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Faible effet de glissement" }, - "swapLowSlippageError": { - "message": "La transaction peut échouer, car le glissement maximal est trop faible." - }, "swapMaxSlippage": { "message": "Glissement maximal" }, @@ -5438,9 +5331,6 @@ "message": "Différence de prix de ~$1", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "L’incidence sur les prix correspond à la différence entre le prix actuel du marché et le montant reçu lors de l’exécution de la transaction. Cette répercussion dépend du volume de votre transaction par rapport au volume de la réserve de liquidités." - }, "swapPriceUnavailableDescription": { "message": "L’incidence sur les prix n’a pas pu être déterminée faute de données suffisantes sur les prix du marché. Veuillez confirmer que vous êtes satisfait·e du nombre de jetons que vous êtes sur le point de recevoir avant de procéder au swap." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Demande de cotation" }, - "swapReviewSwap": { - "message": "Vérifier le swap" - }, - "swapSearchNameOrAddress": { - "message": "Rechercher le nom ou coller l’adresse" - }, "swapSelect": { "message": "Sélectionner" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Faible effet de glissement" }, - "swapSlippageNegative": { - "message": "Le glissement doit être supérieur ou égal à zéro" - }, "swapSlippageNegativeDescription": { "message": "Le slippage doit être supérieur ou égal à zéro" }, @@ -5596,20 +5477,6 @@ "message": "Swap de $1 vers $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Ce jeton a été ajouté manuellement." - }, - "swapTokenVerificationMessage": { - "message": "Confirmez toujours l’adresse du jeton sur $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Vérification effectuée uniquement sur 1 source." - }, - "swapTokenVerificationSources": { - "message": "Vérification effectuée sur $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 n’a été vérifié que par 1 source. Envisagez de le vérifier auprès de $2 sources avant de continuer.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Inconnu" }, - "swapVerifyTokenExplanation": { - "message": "Attention, plusieurs jetons peuvent utiliser le même nom et le même symbole. Vérifiez $1 pour vous assurer qu’il s’agit bien du jeton que vous recherchez.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibles pour un swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de glissement" }, - "swapsAdvancedOptions": { - "message": "Options avancées" - }, - "swapsExcessiveSlippageWarning": { - "message": "Le montant du glissement est trop élevé et donnera lieu à un mauvais taux. Veuillez réduire votre tolérance de glissement à une valeur inférieure à 15 %." - }, "swapsMaxSlippage": { "message": "Tolérance au slippage" }, - "swapsNotEnoughForTx": { - "message": "Pas assez de $1 pour effectuer cette transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Pas assez de $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "La décimale du jeton est requise. Trouvez-la sur : $1" }, - "tokenDecimalTitle": { - "message": "Nombre de décimales du token :" - }, "tokenDetails": { "message": "Détails du token" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Demande de mise à jour" }, - "updatedWithDate": { - "message": "Mis à jour $1" - }, "uploadDropFile": { "message": "Déposez votre fichier ici" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Vérifier les informations relatives aux tiers" }, - "verifyThisTokenOn": { - "message": "Vérifier ce jeton sur $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Vérifiez ce jeton sur $1 et qu’il s’agit bien de celui que vous souhaitez échanger.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, @@ -6229,26 +6064,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..0e624b4ba807 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "संवेदनशील जानकारी छिपाएं" }, - "hideToken": { - "message": "टोकन छुपा दें" - }, "hideTokenPrompt": { "message": "टोकन छिपाएं?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "जैज़आइकन्स" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON फाइल", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "दी गई खोज क्वेरी के लिए कोई अकाउंट नहीं मिला" }, - "noConnectedAccountDescription": { - "message": "जारी रखने के लिए जिस अकाउंट को आप इस साइट पर उपयोग करना चाहते हैं वह अकाउंट चुनें।" - }, "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, - "noConversionDateAvailable": { - "message": "कोई करेंसी कन्वर्शन तारीख उपलब्ध नहीं है" - }, "noConversionRateAvailable": { "message": "कोई भी कन्वर्शन दर उपलब्ध नहीं है" }, @@ -3501,10 +3434,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 +3850,6 @@ "priceUnavailable": { "message": "प्राइस अनुपलब्ध है" }, - "primaryCurrencySetting": { - "message": "प्राथमिक मुद्रा" - }, - "primaryCurrencySettingDescription": { - "message": "चेन की ओरिजिनल करेंसी (जैसे ETH) में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए ओरिजिनल को चुनें। अपनी चुना गया फिएट करेंसी में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए फिएट को चुनें।" - }, "primaryType": { "message": "प्राइमरी टाइप" }, @@ -4151,9 +4074,6 @@ "rejected": { "message": "रिजेक्ट" }, - "remember": { - "message": "याद रखें:" - }, "remove": { "message": "हटाएं" }, @@ -4561,9 +4481,6 @@ "sepolia": { "message": "Sepolia टेस्ट नेटवर्क" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask उत्पाद की उपयोगिता और सुरक्षा को बढ़ाने के लिए इन विश्वसनीय तीसरे-पक्ष की सेवाओं का इस्तेमाल करता है।" - }, "setApprovalForAll": { "message": "सभी के लिए स्वीकृति सेट करें" }, @@ -4753,13 +4670,6 @@ "smartTransactionSuccess": { "message": "आपका ट्रांसेक्शन पूरा हो गया है" }, - "smartTransactionTakingTooLong": { - "message": "माफ़ी चाहते हैं कि आपको इंतज़ार करना पड़ा" - }, - "smartTransactionTakingTooLongDescription": { - "message": "यदि आपका ट्रांसेक्शन $1 के भीतर फाइनलाइज़ नहीं होता है, तो इसे कैंसिल कर दिया जाएगा और आपसे गैस फ़ीस नहीं ली जाएगी।", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "स्मार्ट ट्रांसेक्शन" }, @@ -5176,10 +5086,6 @@ "stxCancelledSubDescription": { "message": "अपना स्वैप फिर से कोशिश करें। अगली बार भी इस तरह के जोखिमों से आपको बचाने के लिए हम यहां होंगे।" }, - "stxEstimatedCompletion": { - "message": "<$1 में पूरा होने का अनुमान", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "स्वैप नहीं हो पाया" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "जब आप जारी रखना चाहते हैं तो हम आपको लेटेस्ट उद्धरण दिखाने के लिए तैयार हैं" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1 के मिलान वाले कोई भी टोकन उपलब्ध नहीं हैं", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "अपने hardware wallet से कन्फर्म करें" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "उद्धरण प्राप्त करने में गड़बड़ी" }, - "swapFetchingTokens": { - "message": "टोकन प्राप्त किए जा रहे हैं..." - }, "swapFromTo": { "message": "$1 से $2 का स्वैप", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "अधिक स्लिपेज" }, - "swapHighSlippageWarning": { - "message": "स्लिपेज अमाउंट बहुत अधिक है।" - }, "swapIncludesMMFee": { "message": "$1% MetaMask फ़ीस शामिल है।", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "कम स्लिपेज" }, - "swapLowSlippageError": { - "message": "ट्रांसेक्शन विफल हो सकता है, अधिकतम स्लिपेज बहुत कम है।" - }, "swapMaxSlippage": { "message": "अधिकतम स्लिपेज" }, @@ -5438,9 +5331,6 @@ "message": "~$1% का मूल्य अंतर", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "मूल्य प्रभाव, वर्तमान बाजार मूल्य और ट्रांसेक्शन निष्पादन के दौरान प्राप्त अमाउंट के बीच का अंतर है। मूल्य प्रभाव चलनिधि पूल के आकार के सापेक्ष आपके व्यापार के आकार का एक कार्य है।" - }, "swapPriceUnavailableDescription": { "message": "बाजार मूल्य डेटा की कमी के कारण मूल्य प्रभाव को निर्धारित नहीं किया जा सका। कृपया कन्फर्म करें कि आप स्वैप करने से पहले प्राप्त होने वाले टोकन की अमाउंट को लेकर सहज हैं।" }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "उद्धरण के लिए अनुरोध" }, - "swapReviewSwap": { - "message": "स्वैप की समीक्षा करें" - }, - "swapSearchNameOrAddress": { - "message": "नाम खोजें या ऐड्रेस पेस्ट करें" - }, "swapSelect": { "message": "चयन करें" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "कम स्लिपेज" }, - "swapSlippageNegative": { - "message": "स्लिपेज शून्य से अधिक या बराबर होना चाहिए" - }, "swapSlippageNegativeDescription": { "message": "स्लिपेज शून्य से अधिक या बराबर होना चाहिए" }, @@ -5596,20 +5477,6 @@ "message": "$1 से $2 में स्वैप करें", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "इस टोकन को मैन्युअल रूप से जोड़ा गया है।" - }, - "swapTokenVerificationMessage": { - "message": "हमेशा $1 पर टोकन एड्रेस की कन्फर्म करें।", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "केवल 1 स्रोत पर वेरीफ़ाई।" - }, - "swapTokenVerificationSources": { - "message": "$1 स्रोतों पर वेरीफ़ाई।", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 केवल 1 स्रोत पर वेरीफ़ाई है। आगे बढ़ने से पहले इसे $2 पर वेरीफ़ाई करने पर विचार करें।", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "अज्ञात" }, - "swapVerifyTokenExplanation": { - "message": "एकाधिक टोकन एक ही नाम और प्रतीक का इस्तेमाल कर सकते हैं। यह वेरीफ़ाई करने के लिए $1 देखें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 स्वैप के लिए उपलब्ध है", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% स्लिपेज" }, - "swapsAdvancedOptions": { - "message": "एडवांस्ड विकल्प" - }, - "swapsExcessiveSlippageWarning": { - "message": "स्लिपेज अमाउंट बहुत अधिक है और इस वजह से खराब दर होगी। कृपया अपने स्लिपेज टॉलरेंस को 15% से नीचे के वैल्यू तक कम करें।" - }, "swapsMaxSlippage": { "message": "स्लिपेज टॉलरेंस" }, - "swapsNotEnoughForTx": { - "message": "इस ट्रांसेक्शन को पूरा करने के लिए $1 कम है", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 कम", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "टोकन डेसीमल की आवश्यकता है। इसे: $1 पर पाएं" }, - "tokenDecimalTitle": { - "message": "टोकन डेसीमल:" - }, "tokenDetails": { "message": "टोकन विवरण" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "अपडेट का अनुरोध" }, - "updatedWithDate": { - "message": "अपडेट किया गया $1" - }, "uploadDropFile": { "message": "अपनी फ़ाइल यहां छोड़ें" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "थर्ड-पार्टी विवरण वेरीफ़ाई करें" }, - "verifyThisTokenOn": { - "message": "इस टोकन को $1 पर वेरीफ़ाई करें", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "इस टोकन को $1 पर वेरीफ़ाई करें और पक्का करें कि यह वही टोकन है जिससे आप ट्रेड करना चाहते हैं।", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "वर्शन" }, @@ -6229,26 +6064,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..68b52556c3f6 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Sembunyikan informasi sensitif" }, - "hideToken": { - "message": "Sembunyikan token" - }, "hideTokenPrompt": { "message": "Sembunyikan token?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "File JSON", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Tidak ditemukan akun untuk kueri pencarian yang diberikan" }, - "noConnectedAccountDescription": { - "message": "Pilih akun yang ingin Anda gunakan di situs ini untuk melanjutkan." - }, "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, - "noConversionDateAvailable": { - "message": "Tanggal konversi mata uang tidak tersedia" - }, "noConversionRateAvailable": { "message": "Nilai konversi tidak tersedia" }, @@ -3501,10 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Ditolak" }, - "remember": { - "message": "Ingatlah:" - }, "remove": { "message": "Hapus" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Kami siap menampilkan kuotasi terbaru jika Anda ingin melanjutkan" }, - "swapBuildQuotePlaceHolderText": { - "message": "Tidak ada token yang cocok yang tersedia $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Konfirmasikan dengan dompet perangkat keras Anda" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Terjadi kesalahan saat mengambil kuota" }, - "swapFetchingTokens": { - "message": "Mengambil token..." - }, "swapFromTo": { "message": "Pertukaran dari $1 ke $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Selip tinggi" }, - "swapHighSlippageWarning": { - "message": "Jumlah slippage sangat tinggi." - }, "swapIncludesMMFee": { "message": "Termasuk $1% biaya MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Selip rendah" }, - "swapLowSlippageError": { - "message": "Transaksi berpotensi gagal, selip maks terlalu rendah." - }, "swapMaxSlippage": { "message": "Selipi maks" }, @@ -5438,9 +5331,6 @@ "message": "Perbedaan harga ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Dampak harga adalah selisih antara harga pasar saat ini dan jumlah yang diterima selama terjadinya transaksi. Dampak harga adalah fungsi ukuran dagang relatif terhadap ukuran pul likuiditas." - }, "swapPriceUnavailableDescription": { "message": "Dampak harga tidak dapat ditentukan karena kurangnya data harga pasar. Harap konfirmasi bahwa Anda setuju dengan jumlah token yang akan Anda terima sebelum penukaran." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Minta kuotasi" }, - "swapReviewSwap": { - "message": "Tinjau pertukaran" - }, - "swapSearchNameOrAddress": { - "message": "Cari nama atau tempel alamat" - }, "swapSelect": { "message": "Pilih" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Selip rendah" }, - "swapSlippageNegative": { - "message": "Selip harus lebih besar atau sama dengan nol" - }, "swapSlippageNegativeDescription": { "message": "Selip harus lebih besar atau sama dengan nol" }, @@ -5596,20 +5477,6 @@ "message": "Tukar $1 ke $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Token ini telah ditambahkan secara manual." - }, - "swapTokenVerificationMessage": { - "message": "Selalu konfirmasikan alamat token di $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Hanya diverifikasi di 1 sumber." - }, - "swapTokenVerificationSources": { - "message": "Diverifikasi di $1 sumber.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 hanya diverifikasi di 1 sumber. Pertimbangkan untuk memverifikasinya di $2 sebelum melanjutkan.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Tidak diketahui" }, - "swapVerifyTokenExplanation": { - "message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 tersedia untuk ditukar", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "Selip 0%" }, - "swapsAdvancedOptions": { - "message": "Opsi lanjutan" - }, - "swapsExcessiveSlippageWarning": { - "message": "Jumlah selip terlalu tinggi dan akan mengakibatkan tarif yang buruk. Kurangi toleransi selip Anda ke nilai di bawah 15%." - }, "swapsMaxSlippage": { "message": "Toleransi selip" }, - "swapsNotEnoughForTx": { - "message": "$1 tidak cukup untuk menyelesaikan transaksi ini", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 tidak cukup", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Desimal token diperlukan. Temukan di: $1" }, - "tokenDecimalTitle": { - "message": "Desimal token:" - }, "tokenDetails": { "message": "Detail token" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Permintaan pembaruan" }, - "updatedWithDate": { - "message": "Diperbarui $1" - }, "uploadDropFile": { "message": "Letakkan fail di sini" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Verifikasikan detail pihak ketiga" }, - "verifyThisTokenOn": { - "message": "Verifikasikan token ini di $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifikasikan token ini di $1 dan pastikan ini adalah token yang ingin Anda perdagangkan.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versi" }, @@ -6229,26 +6064,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..4dac80c253b3 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" }, @@ -1409,10 +1387,6 @@ "message": "Devi avere $1 $2 in più per completare lo scambio", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Non ci sono token disponibile con questo nome $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapCustom": { "message": "personalizza" }, @@ -1441,12 +1415,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Errore recuperando le quotazioni" }, - "swapFetchingTokens": { - "message": "Recuperando i token..." - }, - "swapLowSlippageError": { - "message": "La transazione può fallire, il massimo slippage è troppo basso." - }, "swapMaxSlippage": { "message": "Slippage massimo" }, @@ -1506,9 +1474,6 @@ "swapRequestForQuotation": { "message": "Richiedi quotazione" }, - "swapReviewSwap": { - "message": "Verifica Scambio" - }, "swapSelect": { "message": "Selezione" }, @@ -1541,44 +1506,15 @@ "message": "Scambia da $1 a $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationMessage": { - "message": "Verifica sempre l'indirizzo del token su $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificato solo su una fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificato su $1 fonti.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTransactionComplete": { "message": "Transazione completata" }, "swapUnknown": { "message": "Sconosciuto" }, - "swapVerifyTokenExplanation": { - "message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibili allo scambio", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, - "swapsAdvancedOptions": { - "message": "Impostazioni Avanzate" - }, - "swapsExcessiveSlippageWarning": { - "message": "L'importo di slippage è troppo alto e risulterà in una tariffa sconveniente. Riduci la tolleranza allo slippage ad un valore minore di 15%." - }, "swapsMaxSlippage": { "message": "Tolleranza Slippage" }, - "swapsNotEnoughForTx": { - "message": "Non hai abbastanza $1 per completare la transazione", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Vedi in attività" }, @@ -1698,9 +1634,6 @@ "unlockMessage": { "message": "Il web decentralizzato ti attende" }, - "updatedWithDate": { - "message": "Aggiornata $1" - }, "urlErrorMsg": { "message": "Gli URI richiedono un prefisso HTTP/HTTPS." }, @@ -1716,10 +1649,6 @@ "userName": { "message": "Nome utente" }, - "verifyThisTokenOn": { - "message": "Verifica questo token su $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Vedi tutti i dettagli" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index e699ccf2dc09..73f3f8300646 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "機密情報を非表示" }, - "hideToken": { - "message": "トークンを非表示" - }, "hideTokenPrompt": { "message": "トークンを非表示にしますか?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicon" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSONファイル", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "指定された検索クエリでアカウントが見つかりませんでした" }, - "noConnectedAccountDescription": { - "message": "続行するには、このサイトで使用するアカウントを選択してください。" - }, "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, - "noConversionDateAvailable": { - "message": "通貨換算日がありません" - }, "noConversionRateAvailable": { "message": "利用可能な換算レートがありません" }, @@ -3501,10 +3434,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 +3850,6 @@ "priceUnavailable": { "message": "価格が利用できません" }, - "primaryCurrencySetting": { - "message": "プライマリ通貨" - }, - "primaryCurrencySettingDescription": { - "message": "チェーンのネイティブ通貨 (ETHなど) による値の表示を優先するには、「ネイティブ」を選択します。選択した法定通貨による値の表示を優先するには、「法定通貨」を選択します。" - }, "primaryType": { "message": "基本型" }, @@ -4151,9 +4074,6 @@ "rejected": { "message": "拒否されました" }, - "remember": { - "message": "ご注意:" - }, "remove": { "message": "削除" }, @@ -4561,9 +4481,6 @@ "sepolia": { "message": "Sepoliaテストネットワーク" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMaskはこれらの信頼できるサードパーティサービスを使用して、製品の使いやすさと安全性を向上させています。" - }, "setApprovalForAll": { "message": "すべてを承認に設定" }, @@ -4753,13 +4670,6 @@ "smartTransactionSuccess": { "message": "トランザクションが完了しました" }, - "smartTransactionTakingTooLong": { - "message": "お待たせして申し訳ございません" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1以内にトランザクションが完了しない場合はキャンセルされ、ガス代は請求されません。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "スマートトランザクション" }, @@ -5176,10 +5086,6 @@ "stxCancelledSubDescription": { "message": "もう一度スワップをお試しください。次回は同様のリスクを避けられるようサポートします。" }, - "stxEstimatedCompletion": { - "message": "$1未満で完了予定", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "スワップに失敗しました" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "続ける際には、最新のクォートを表示する準備ができています" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1と一致するトークンがありません", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "ハードウェアウォレットで確定" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "見積もり取得エラー" }, - "swapFetchingTokens": { - "message": "トークンを取得中..." - }, "swapFromTo": { "message": "$1から$2へのスワップ", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "高スリッページ" }, - "swapHighSlippageWarning": { - "message": "スリッページが非常に大きいです。" - }, "swapIncludesMMFee": { "message": "$1%のMetaMask手数料が含まれています。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "低スリッページ" }, - "swapLowSlippageError": { - "message": "トランザクションが失敗する可能性があります。最大スリッページが低すぎます。" - }, "swapMaxSlippage": { "message": "最大スリッページ" }, @@ -5438,9 +5331,6 @@ "message": "最大$1%の価格差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "プライスインパクトとは、現在の市場価格と取引の約定時に受け取る金額の差のことです。プライスインパクトは、流動性プールに対する取引の大きさにより発生します。" - }, "swapPriceUnavailableDescription": { "message": "市場価格のデータが不足しているため、プライスインパクトを測定できませんでした。スワップする前に、これから受領するトークンの額に問題がないか確認してください。" }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "見積もりのリクエスト" }, - "swapReviewSwap": { - "message": "スワップの確認" - }, - "swapSearchNameOrAddress": { - "message": "名前を検索するかアドレスを貼り付けてください" - }, "swapSelect": { "message": "選択" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "低スリッページ" }, - "swapSlippageNegative": { - "message": "スリッページは0以上でなければなりません。" - }, "swapSlippageNegativeDescription": { "message": "スリッページは0以上でなければなりません" }, @@ -5596,20 +5477,6 @@ "message": "$1を$2にスワップ", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "このトークンは手動で追加されました。" - }, - "swapTokenVerificationMessage": { - "message": "常に$1のトークンアドレスを確認してください。", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "1つのソースでのみ検証済みです。" - }, - "swapTokenVerificationSources": { - "message": "$1個のソースで検証済みです。", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1は1つのソースでしか検証されていません。進める前に$2で検証することをご検討ください。", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "不明" }, - "swapVerifyTokenExplanation": { - "message": "複数のトークンで同じ名前とシンボルを使用できます。$1をチェックして、これが探しているトークンであることを確認してください。", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2がスワップに使用可能です", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0%スリッページ" }, - "swapsAdvancedOptions": { - "message": "詳細オプション" - }, - "swapsExcessiveSlippageWarning": { - "message": "スリッページ額が非常に大きいので、レートが不利になります。最大スリッページを15%未満の値に減らしてください。" - }, "swapsMaxSlippage": { "message": "最大スリッページ" }, - "swapsNotEnoughForTx": { - "message": "トランザクションを完了させるには、$1が不足しています", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1が不足しています", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "トークンの小数点以下の桁数が必要です。確認はこちら: $1" }, - "tokenDecimalTitle": { - "message": "トークンの小数桁数:" - }, "tokenDetails": { "message": "トークンの詳細" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "更新リクエスト" }, - "updatedWithDate": { - "message": "$1が更新されました" - }, "uploadDropFile": { "message": "ここにファイルをドロップします" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "サードパーティの詳細を確認" }, - "verifyThisTokenOn": { - "message": "このトークンを$1で検証", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "このトークンを$1で検証して、取引したいトークンであることを確認してください。", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "バージョン" }, @@ -6229,26 +6064,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..be1de55c51c7 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "민감한 정보 숨기기" }, - "hideToken": { - "message": "토큰 숨기기" - }, "hideTokenPrompt": { "message": "토큰을 숨기겠습니까?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON 파일", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "검색어에 해당하는 계정이 없습니다." }, - "noConnectedAccountDescription": { - "message": "이 사이트에서 계속 사용하고자 하는 계정을 선택하세요." - }, "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, - "noConversionDateAvailable": { - "message": "사용 가능한 통화 변환 날짜 없음" - }, "noConversionRateAvailable": { "message": "사용 가능한 환율 없음" }, @@ -3501,10 +3434,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 +3850,6 @@ "priceUnavailable": { "message": "가격 사용 불가" }, - "primaryCurrencySetting": { - "message": "기본 통화" - }, - "primaryCurrencySettingDescription": { - "message": "체인의 고유 통화(예: ETH)로 값을 우선 표시하려면 고유를 선택합니다. 선택한 명목 통화로 값을 우선 표시하려면 명목을 선택합니다." - }, "primaryType": { "message": "기본 유형" }, @@ -4151,9 +4074,6 @@ "rejected": { "message": "거부됨" }, - "remember": { - "message": "참고:" - }, "remove": { "message": "제거" }, @@ -4561,9 +4481,6 @@ "sepolia": { "message": "Sepolia 테스트 네트워크" }, - "setAdvancedPrivacySettingsDetails": { - "message": "이와 같이 MetaMask는 신용있는 타사의 서비스를 사용하여 제품 가용성과 안전성을 향상합니다." - }, "setApprovalForAll": { "message": "모두 승인 설정" }, @@ -4753,13 +4670,6 @@ "smartTransactionSuccess": { "message": "트랜잭션 완료" }, - "smartTransactionTakingTooLong": { - "message": "기다리게 해서 죄송합니다" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1 이내에 트랜잭션이 완료되지 않으면 트랜잭션이 취소되고 가스비가 부과되지 않습니다.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "스마트 트랜잭션" }, @@ -5176,10 +5086,6 @@ "stxCancelledSubDescription": { "message": "스왑을 다시 진행하세요. 다음에도 유사한 위험이 발생한다면 보호해 드리겠습니다." }, - "stxEstimatedCompletion": { - "message": "예상 잔여 시간: <$1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "스왑 실패" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "계속하기 원하시면 최신 견적을 보여드리겠습니다" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1와(과) 일치하는 토큰이 없습니다.", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "하드웨어 지갑으로 컨펌합니다." }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "견적을 가져오는 중 오류 발생" }, - "swapFetchingTokens": { - "message": "토큰 가져오는 중..." - }, "swapFromTo": { "message": "$1을(를) $2(으)로 스왑", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "높은 슬리피지" }, - "swapHighSlippageWarning": { - "message": "슬리패지 금액이 아주 큽니다." - }, "swapIncludesMMFee": { "message": "$1%의 MetaMask 요금이 포함됩니다.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "낮은 슬리피지" }, - "swapLowSlippageError": { - "message": "트랜잭션이 실패할 수도 있습니다. 최대 슬리패지가 너무 낮습니다." - }, "swapMaxSlippage": { "message": "최대 슬리패지" }, @@ -5438,9 +5331,6 @@ "message": "~$1%의 가격 차이", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "가격 영향은 현재 시장 가격과 트랜잭션 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 트랜잭션의 크기를 나타내는 함수입니다." - }, "swapPriceUnavailableDescription": { "message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수가 만족스러운지 컨펌하시기 바랍니다." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "견적 요청" }, - "swapReviewSwap": { - "message": "스왑 검토" - }, - "swapSearchNameOrAddress": { - "message": "이름 검색 또는 주소 붙여넣기" - }, "swapSelect": { "message": "선택" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "낮은 슬리피지" }, - "swapSlippageNegative": { - "message": "슬리패지는 0보다 크거나 같아야 합니다." - }, "swapSlippageNegativeDescription": { "message": "슬리피지는 0보다 크거나 같아야 합니다." }, @@ -5596,20 +5477,6 @@ "message": "$1에서 $2(으)로 스왑", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "이 토큰은 직접 추가되었습니다." - }, - "swapTokenVerificationMessage": { - "message": "항상 $1에서 토큰 주소를 컨펌하세요.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "1개의 소스에서만 확인됩니다." - }, - "swapTokenVerificationSources": { - "message": "$1개 소스에서 확인되었습니다.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 토큰은 1 소스에서만 확인됩니다. 계속 진행하기 전에 $2에서도 확인하세요.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "알 수 없음" }, - "swapVerifyTokenExplanation": { - "message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1에서 원하는 토큰인지 확인하세요.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 스왑 가능", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% 슬리패지" }, - "swapsAdvancedOptions": { - "message": "고급 옵션" - }, - "swapsExcessiveSlippageWarning": { - "message": "슬리패지 금액이 너무 커서 전환율이 좋지 않습니다. 슬리패지 허용치를 15% 값 이하로 줄이세요." - }, "swapsMaxSlippage": { "message": "슬리피지 허용치" }, - "swapsNotEnoughForTx": { - "message": "$1이(가) 부족하여 이 트랜잭션을 완료할 수 없습니다.", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 부족", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "토큰 십진수가 필요합니다. $1에서 찾아보세요" }, - "tokenDecimalTitle": { - "message": "토큰 소수점:" - }, "tokenDetails": { "message": "토큰 상세 정보" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "업데이트 요청" }, - "updatedWithDate": { - "message": "$1에 업데이트됨" - }, "uploadDropFile": { "message": "여기에 파일을 드롭" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "타사 세부 정보 확인" }, - "verifyThisTokenOn": { - "message": "$1에서 이 토큰 확인", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "$1에서 이 토큰이 트랜잭션할 토큰이 맞는지 확인하세요.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "버전" }, @@ -6229,26 +6064,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..1687cb7818f0 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" }, @@ -1290,10 +1268,6 @@ "message": "Kailangan mo ng $1 pa $2 para makumpleto ang pag-swap na ito", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Walang available na token na tumutugma sa $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin ang iyong hardware wallet" }, @@ -1335,9 +1309,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Nagka-error sa pagkuha ng mga quote" }, - "swapFetchingTokens": { - "message": "Kinukuha ang mga token..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1345,12 +1316,6 @@ "swapGasFeesSplit": { "message": "Hahatiin sa pagitan ng dalawang transaksyon na ito ang mga bayarin sa gas sa nakaraang screen." }, - "swapHighSlippageWarning": { - "message": "Sobrang laki ng halaga ng slippage." - }, - "swapLowSlippageError": { - "message": "Posibleng hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." - }, "swapMaxSlippage": { "message": "Max na slippage" }, @@ -1377,9 +1342,6 @@ "message": "Kaibahan sa presyo na ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Ang epekto sa presyo ay ang pagkakaiba sa kasalukuyang presyo sa merkado at sa halagang natanggap sa pag-execute ng transaksyon. Ang epekto sa presyo ay isang function ng laki ng iyong trade kumpara sa laki ng liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Hindi natukoy ang epekto sa presyo dahil sa kakulangan ng data sa presyo sa merkado. Pakikumpirma na kumportable ka sa dami ng mga token na matatanggap mo bago makipag-swap." }, @@ -1419,9 +1381,6 @@ "swapRequestForQuotation": { "message": "Mag-request ng quotation" }, - "swapReviewSwap": { - "message": "Suriin ang Pag-swap" - }, "swapSelect": { "message": "Piliin" }, @@ -1434,9 +1393,6 @@ "swapSelectQuotePopoverDescription": { "message": "Makikita sa ibaba ang lahat ng quote na nakuha mula sa maraming pinagkukunan ng liquidity." }, - "swapSlippageNegative": { - "message": "Dapat ay mas malaki sa o katumbas ng zero ang slippage" - }, "swapSource": { "message": "Pinagkunan ng liquidity" }, @@ -1464,20 +1420,6 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Manual na idinagdag ang token na ito." - }, - "swapTokenVerificationMessage": { - "message": "Palaging kumpirmahin ang address ng token sa $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Na-verify lang sa 1 source." - }, - "swapTokenVerificationSources": { - "message": "Na-verify sa $1 source.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTransactionComplete": { "message": "Nakumpleto ang transaksyon" }, @@ -1487,30 +1429,12 @@ "swapUnknown": { "message": "Hindi Alam" }, - "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Available ang $1 $2 na i-swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Mga Advanced na Opsyon" - }, - "swapsExcessiveSlippageWarning": { - "message": "Masyadong mataas ang halaga ng slippage at magreresulta ito sa masamang rating. Pakibabaan ang iyong tolerance ng slippage sa value na mas mababa sa 15%." - }, "swapsMaxSlippage": { "message": "Tolerance ng Slippage" }, - "swapsNotEnoughForTx": { - "message": "Hindi sapat ang $1 para makumpleto ang transaksyong ito", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Tingnan sa aktibidad" }, @@ -1655,9 +1579,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." }, @@ -1673,14 +1594,6 @@ "userName": { "message": "Username" }, - "verifyThisTokenOn": { - "message": "I-verify ang token na ito sa $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "I-verify ang token na ito sa $1 at tiyaking ito ang token na gusto mong i-trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Tingnan ang lahat ng detalye" }, diff --git a/app/_locales/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..656733cecf65 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Ocultar informações confidenciais" }, - "hideToken": { - "message": "Ocultar token" - }, "hideTokenPrompt": { "message": "Ocultar token?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Arquivo JSON", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Nenhuma conta encontrada para a pesquisa efetuada" }, - "noConnectedAccountDescription": { - "message": "Selecione uma conta que você deseja usar neste site para continuar." - }, "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, - "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 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Recusada" }, - "remember": { - "message": "Lembre-se:" - }, "remove": { "message": "Remover" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Estamos prontos para exibir as últimas cotações quando quiser continuar" }, - "swapBuildQuotePlaceHolderText": { - "message": "Nenhum token disponível correspondente a $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erro ao obter cotações" }, - "swapFetchingTokens": { - "message": "Obtendo tokens..." - }, "swapFromTo": { "message": "A troca de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Slippage alto" }, - "swapHighSlippageWarning": { - "message": "O valor de slippage está muito alto." - }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Slippage baixo" }, - "swapLowSlippageError": { - "message": "A transação pode falhar; o slippage máximo está baixo demais." - }, "swapMaxSlippage": { "message": "Slippage máximo" }, @@ -5438,9 +5331,6 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "O impacto do preço é a diferença entre o preço de mercado atual e o valor recebido quando é executada a transação. O impacto do preço é resultado do tamanho da sua transação relativo ao tamanho do pool de liquidez." - }, "swapPriceUnavailableDescription": { "message": "O impacto no preço não pôde ser determinado devido à ausência de dados sobre o preço de mercado. Confirme que você está satisfeito com a quantidade de tokens que você está prestes a receber antes de fazer a troca." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Solicitação de cotação" }, - "swapReviewSwap": { - "message": "Revisar troca" - }, - "swapSearchNameOrAddress": { - "message": "Pesquise o nome ou cole o endereço" - }, "swapSelect": { "message": "Selecione" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Slippage baixo" }, - "swapSlippageNegative": { - "message": "O slippage deve ser maior ou igual a zero" - }, "swapSlippageNegativeDescription": { "message": "O slippage deve ser maior ou igual a zero" }, @@ -5596,20 +5477,6 @@ "message": "Trocar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Esse token foi adicionado manualmente." - }, - "swapTokenVerificationMessage": { - "message": "Sempre confirme o endereço do token no $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificado somente em 1 fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificado em $1 fontes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 só foi verificado em 1 fonte. Considere verificá-lo em $2 antes de prosseguir.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Desconhecido" }, - "swapVerifyTokenExplanation": { - "message": "Vários tokens podem usar o mesmo nome e símbolo. Confira $1 para verificar se esse é o token que você está buscando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponível para troca", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% de slippage" }, - "swapsAdvancedOptions": { - "message": "Opções avançadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "O valor de slippage está muito alto e resultará em uma taxa ruim. Reduza sua tolerância a slippage para um valor inferior a 15%." - }, "swapsMaxSlippage": { "message": "Tolerância a slippage" }, - "swapsNotEnoughForTx": { - "message": "Não há $1 suficiente para concluir essa transação", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 insuficiente", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "É necessário o decimal do token. Encontre-o em: $1" }, - "tokenDecimalTitle": { - "message": "Decimal do token:" - }, "tokenDetails": { "message": "Dados do token" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Solicitação de atualização" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "uploadDropFile": { "message": "Solte seu arquivo aqui" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Verificar dados do terceiro" }, - "verifyThisTokenOn": { - "message": "Verifique esse token no $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique esse token no $1 e confirme que é o token que você deseja negociar.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versão" }, @@ -6229,26 +6064,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..6062013d6d6f 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" }, @@ -1932,10 +1894,6 @@ "message": "Você precisa de mais $1 $2 para concluir essa troca", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Nenhum token disponível correspondente a $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -1987,9 +1945,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erro ao obter cotações" }, - "swapFetchingTokens": { - "message": "Obtendo tokens..." - }, "swapFromTo": { "message": "A troca de $1 para $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -2007,16 +1962,10 @@ "message": "As taxas de gás são pagas aos mineradores de criptoativos que processam as transações na rede de $1. A MetaMask não lucra com taxas de gás.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, - "swapHighSlippageWarning": { - "message": "O valor de slippage está muito alto." - }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapLowSlippageError": { - "message": "A transação pode falhar; o slippage máximo está baixo demais." - }, "swapMaxSlippage": { "message": "Slippage máximo" }, @@ -2047,9 +1996,6 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "O impacto no preço é a diferença entre o preço de mercado atual e o valor recebido durante a execução da transação. O impacto no preço é uma função do porte da sua operação em relação ao porte do pool de liquidez." - }, "swapPriceUnavailableDescription": { "message": "O impacto no preço não pôde ser determinado devido à ausência de dados sobre o preço de mercado. Confirme que você está satisfeito com a quantidade de tokens que você está prestes a receber antes de fazer a troca." }, @@ -2089,9 +2035,6 @@ "swapRequestForQuotation": { "message": "Solicitação de cotação" }, - "swapReviewSwap": { - "message": "Revisar troca" - }, "swapSelect": { "message": "Selecione" }, @@ -2104,9 +2047,6 @@ "swapSelectQuotePopoverDescription": { "message": "Abaixo estão todas as cotações reunidas de diversas fontes de liquidez." }, - "swapSlippageNegative": { - "message": "O slippage deve ser maior ou igual a zero" - }, "swapSource": { "message": "Fonte de liquidez" }, @@ -2140,20 +2080,6 @@ "message": "Trocar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token foi adicionado manualmente." - }, - "swapTokenVerificationMessage": { - "message": "Sempre confirme o endereço do token no $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificado somente em 1 fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificado em $1 fontes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTooManyDecimalsError": { "message": "$1 permite até $2 decimais", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -2167,30 +2093,12 @@ "swapUnknown": { "message": "Desconhecido" }, - "swapVerifyTokenExplanation": { - "message": "Vários tokens podem usar o mesmo nome e símbolo. Confira $1 para verificar se esse é o token que você está buscando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponível para troca", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% de slippage" }, - "swapsAdvancedOptions": { - "message": "Opções avançadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "O valor de slippage está muito alto e resultará em uma taxa ruim. Reduza sua tolerância a slippage para um valor inferior a 15%." - }, "swapsMaxSlippage": { "message": "Tolerância a slippage" }, - "swapsNotEnoughForTx": { - "message": "Não há $1 suficiente para concluir essa transação", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Ver na atividade" }, @@ -2409,9 +2317,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." }, @@ -2430,14 +2335,6 @@ "userName": { "message": "Nome de usuário" }, - "verifyThisTokenOn": { - "message": "Verifique esse token no $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique esse token no $1 e confirme que é o token que você deseja negociar.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Ver todos os detalhes" }, @@ -2474,26 +2371,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..0c2f92821ed2 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Скрыть конфиденциальную информацию" }, - "hideToken": { - "message": "Скрыть токен" - }, "hideTokenPrompt": { "message": "Скрыть токен?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON-файл", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "По данному поисковому запросу счетов не найдено" }, - "noConnectedAccountDescription": { - "message": "Для продолжения выберите счет, который вы хотите использовать на этом сайте." - }, "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, - "noConversionDateAvailable": { - "message": "Дата обмена валюты недоступна" - }, "noConversionRateAvailable": { "message": "Нет доступного обменного курса" }, @@ -3501,10 +3434,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 +3850,6 @@ "priceUnavailable": { "message": "цена недоступна" }, - "primaryCurrencySetting": { - "message": "Основная валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Выберите «собственная», чтобы установить приоритет отображения значений в собственной валюте блокчейна (например, ETH). Выберите «Фиатная», чтобы установить приоритет отображения значений в выбранной фиатной валюте." - }, "primaryType": { "message": "Основной тип" }, @@ -4151,9 +4074,6 @@ "rejected": { "message": "Отклонено" }, - "remember": { - "message": "Помните:" - }, "remove": { "message": "Удалить" }, @@ -4561,9 +4481,6 @@ "sepolia": { "message": "Тестовая сеть Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask использует эти доверенные сторонние сервисы для повышения удобства использования и безопасности продукта." - }, "setApprovalForAll": { "message": "Установить одобрение для всех" }, @@ -4753,13 +4670,6 @@ "smartTransactionSuccess": { "message": "Ваша транзакция завершена" }, - "smartTransactionTakingTooLong": { - "message": "Извините за ожидание" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Если ваша транзакция не будет завершена в течение $1, она будет отменена и с вас не будет взиматься плата за газ.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Умные транзакции" }, @@ -5176,10 +5086,6 @@ "stxCancelledSubDescription": { "message": "Попробуйте выполнить своп еще раз. Мы готовы защитить вас от подобных рисков в следующий раз." }, - "stxEstimatedCompletion": { - "message": "Предполагаемое завершение через < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Своп не удался" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Мы готовы показать вам последние котировки, когда вы захотите продолжить" }, - "swapBuildQuotePlaceHolderText": { - "message": "Нет доступных токенов, соответствующих $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Подтвердите с помощью аппаратного кошелька" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Ошибка при получении котировок" }, - "swapFetchingTokens": { - "message": "Получение токенов..." - }, "swapFromTo": { "message": "Своп $1 на $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Высокое проскальзывание" }, - "swapHighSlippageWarning": { - "message": "Сумма проскальзывания очень велика." - }, "swapIncludesMMFee": { "message": "Включает комиссию MetaMask в размере $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Низкое проскальзывание" }, - "swapLowSlippageError": { - "message": "Возможно, не удастся выполнить транзакцию. Ммаксимальное проскальзывание слишком низкое." - }, "swapMaxSlippage": { "message": "Максимальное проскальзывание" }, @@ -5438,9 +5331,6 @@ "message": "Разница в цене составляет ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Колебание цены — это разница между текущей рыночной ценой и суммой, полученной во время исполнения транзакции. Колебание цены зависит от соотношения размера вашей сделки и размера пула ликвидности." - }, "swapPriceUnavailableDescription": { "message": "Не удалось определить колебание цены из-за отсутствия данных о рыночных ценах. Перед свопом убедитесь, что вас устраивает количество токенов, которое вы получите." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Запрос котировки" }, - "swapReviewSwap": { - "message": "Проверить своп" - }, - "swapSearchNameOrAddress": { - "message": "Выполните поиск по имени или вставьте адрес" - }, "swapSelect": { "message": "Выбрать" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Низкое проскальзывание" }, - "swapSlippageNegative": { - "message": "Проскальзывание должно быть больше нуля или равно нулю" - }, "swapSlippageNegativeDescription": { "message": "Проскальзывание должно быть больше или равно нулю" }, @@ -5596,20 +5477,6 @@ "message": "Выполнить своп $1 на $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Этот токен был добавлен вручную." - }, - "swapTokenVerificationMessage": { - "message": "Всегда проверяйте адрес токена на $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Токен проверен только в 1 источнике." - }, - "swapTokenVerificationSources": { - "message": "Токен проверен в $1 источниках.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 проверяется только на 1 источнике. Попробуйте проверить его на $2, прежде чем продолжить.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Неизвестно" }, - "swapVerifyTokenExplanation": { - "message": "Для обозначения нескольких токенов могут использоваться одно и то же имя и символ. Убедитесь, что $1 — это именно тот токен, который вы ищете.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 доступны для свопа", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% проскальзывания" }, - "swapsAdvancedOptions": { - "message": "Дополнительные параметры" - }, - "swapsExcessiveSlippageWarning": { - "message": "Величина проскальзывания очень велика. Сделка будет невыгодной. Снизьте допуск проскальзывания ниже 15%." - }, "swapsMaxSlippage": { "message": "Допуск проскальзывания" }, - "swapsNotEnoughForTx": { - "message": "Недостаточно $1 для выполнения этой транзакции", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Недостаточно $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Требуется десятичный токен. Найдите его здесь: $1" }, - "tokenDecimalTitle": { - "message": "Десятичный токен:" - }, "tokenDetails": { "message": "Сведения о токене" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Запрос обновления" }, - "updatedWithDate": { - "message": "Обновлено $1" - }, "uploadDropFile": { "message": "Переместите свой файл сюда" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Проверьте информацию о третьей стороне" }, - "verifyThisTokenOn": { - "message": "Проверить этот токен на $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Проверьте этот токен на $1 и убедитесь, что это тот токен, которым вы хотите торговать.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Версия" }, @@ -6229,26 +6064,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..61d8ff6e5d8c 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Itago ang sensitibong impormasyon" }, - "hideToken": { - "message": "Itago ang token" - }, "hideTokenPrompt": { "message": "Itago ang token?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Mga Jazzicon" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "File na JSON", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Walang nakitang account para sa ibinigay na query sa paghahanap" }, - "noConnectedAccountDescription": { - "message": "Pumili ng account na gusto mong gamitin sa site na ito para magpatuloy." - }, "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, - "noConversionDateAvailable": { - "message": "Walang available na petsa sa pagpapapalit ng currency" - }, "noConversionRateAvailable": { "message": "Hindi available ang rate ng palitan" }, @@ -3501,10 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Tinanggihan" }, - "remember": { - "message": "Tandaan:" - }, "remove": { "message": "Alisin" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Handa kaming ipakita sa iyo ang mga pinakabagong quote kapag gusto mo ng magpatuloy" }, - "swapBuildQuotePlaceHolderText": { - "message": "Walang available na token na tumutugma sa $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin gamit ang iyong wallet na hardware" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Nagka-error sa pagkuha ng mga quote" }, - "swapFetchingTokens": { - "message": "Kinukuha ang mga token..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Mataas na slippage" }, - "swapHighSlippageWarning": { - "message": "Napakataas ng halaga ng slippage." - }, "swapIncludesMMFee": { "message": "Kasama ang $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Mababang slippage" }, - "swapLowSlippageError": { - "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." - }, "swapMaxSlippage": { "message": "Max na slippage" }, @@ -5438,9 +5331,6 @@ "message": "Deperensya ng presyo ng ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Ang price impact ay ang pagkakaiba sa pagitan ng kasalukuyang market price at ang halagang natanggap sa panahon ng pagpapatupad ng transaksyon. Ang price impact ay isang function ng laki ng iyong trade kaugnay sa laki ng pool ng liquidity." - }, "swapPriceUnavailableDescription": { "message": "Hindi matukoy ang price impact dahil sa kakulangan ng data ng market price. Pakikumpirma na komportable ka sa dami ng mga token na matatanggap mo bago mag-swap." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Humiling ng quotation" }, - "swapReviewSwap": { - "message": "I-review ang pag-swap" - }, - "swapSearchNameOrAddress": { - "message": "Hanapin ang pangalan o i-paste ang address" - }, "swapSelect": { "message": "Piliin" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Mababang slippage" }, - "swapSlippageNegative": { - "message": "Ang slippage ay dapat mas malaki o katumbas ng zero" - }, "swapSlippageNegativeDescription": { "message": "Dapat na mas malaki o katumbas ng zero ang slippage" }, @@ -5596,20 +5477,6 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Manwal na naidagdag ang token na ito." - }, - "swapTokenVerificationMessage": { - "message": "Palaging kumpirmahin ang token address sa $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Na-verify sa 1 pinagmulan lang." - }, - "swapTokenVerificationSources": { - "message": "Na-verify sa $1 na source.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "Na-verify $1 sa 1 pinagmulan lang. Pag-isipang i-verify ito sa $2 bago magpatuloy.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Hindi Alam" }, - "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Available ang $1 $2 na i-swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Mga Advanced na Opsyon" - }, - "swapsExcessiveSlippageWarning": { - "message": "Masyadong mataas ang halaga ng slippage at magreresulta sa masamang rate. Mangyaring bawasan ang iyong slippage tolerance sa halagang mas mababa sa 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Hindi sapat ang $1 para makumpleto ang transaksyong ito", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Hindi sapat ang $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Kailangan ng decimal ng token. Hanapin ito sa: $1" }, - "tokenDecimalTitle": { - "message": "Mga Decimal ng Katumpakan:" - }, "tokenDetails": { "message": "Mga detalye ng token" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Hiling sa pag-update" }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "uploadDropFile": { "message": "I-drop ang file mo rito" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "I-verify ang mga detalye ng third-party" }, - "verifyThisTokenOn": { - "message": "I-verify ang token na ito sa $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "I-verify ang token na ito sa $1 at siguruhin na ito ang token na gusto mong i-trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Bersyon" }, @@ -6229,26 +6064,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..d80d6564b880 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Hassas bilgileri gizle" }, - "hideToken": { - "message": "Tokeni gizle" - }, "hideTokenPrompt": { "message": "Tokeni gizle?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON Dosyası", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Belirtilen arama sorgusu için hesap bulunamadı" }, - "noConnectedAccountDescription": { - "message": "Devam etmek için bu sitede kullanmak istediğiniz bir hesap seçin." - }, "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, - "noConversionDateAvailable": { - "message": "Para birimi dönüşüm tarihi mevcut değil" - }, "noConversionRateAvailable": { "message": "Dönüşüm oranı mevcut değil" }, @@ -3501,10 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Reddedildi" }, - "remember": { - "message": "Unutmayın:" - }, "remove": { "message": "Kaldır" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Devam etmek istediğinizde size en yeni kotaları göstermeye hazırız" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1 ile eşleşen token yok", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Donanım cüzdanınızla onaylayın" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Teklifler alınırken hata" }, - "swapFetchingTokens": { - "message": "Tokenler alınıyor..." - }, "swapFromTo": { "message": "$1 ile $2 swap işlemi", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Yüksek kayma" }, - "swapHighSlippageWarning": { - "message": "Kayma tutarı çok yüksek." - }, "swapIncludesMMFee": { "message": "%$1 MetaMask ücreti dahildir.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Düşük kayma" }, - "swapLowSlippageError": { - "message": "İşlem başarısız olabilir, maks. kayma çok düşük." - }, "swapMaxSlippage": { "message": "Maks. kayma" }, @@ -5438,9 +5331,6 @@ "message": "~%$1 fiyat farkı", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Fiyat etkisi, mevcut piyasa fiyatı ile işlem gerçekleştirildiği sırada alınan tutar arasındaki farktır. Fiyat etkisi, likidite havuzunun boyutuna bağlı olarak işleminizin boyutunun bir fonksiyonudur." - }, "swapPriceUnavailableDescription": { "message": "Fiyat etkisi, piyasa fiyat verisinin mevcut olmaması nedeniyle belirlenememiştir. Swap işlemini gerçekleştirmeden önce lütfen almak üzere olduğunuz token tutarının sizin için uygun olduğunu onaylayın." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Teklif talebi" }, - "swapReviewSwap": { - "message": "Swap'ı incele" - }, - "swapSearchNameOrAddress": { - "message": "İsmi arayın veya adresi yapıştırın" - }, "swapSelect": { "message": "Seç" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Düşük kayma" }, - "swapSlippageNegative": { - "message": "Kayma en az sıfır olmalıdır" - }, "swapSlippageNegativeDescription": { "message": "Kayma en az sıfır olmalıdır" }, @@ -5596,20 +5477,6 @@ "message": "$1 ile $2 swap gerçekleştir", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Bu token manuel olarak eklendi." - }, - "swapTokenVerificationMessage": { - "message": "Her zaman token adresini $1 üzerinde onaylayın.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Sadece 1 kaynakta doğrulandı." - }, - "swapTokenVerificationSources": { - "message": "$1 kaynakta doğrulandı.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 sadece 1 kaynakta doğrulandı. İlerlemeden önce $2 üzerinde doğrulamayı deneyin.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Bilinmiyor" }, - "swapVerifyTokenExplanation": { - "message": "Birden fazla token aynı adı ve sembolü kullanabilir. Aradığınız tokenin bu olup olmadığını $1 alanında kontrol edin.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 için swap işlemi yapılabilir", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "%0 Kayma" }, - "swapsAdvancedOptions": { - "message": "Gelişmiş seçenekler" - }, - "swapsExcessiveSlippageWarning": { - "message": "Kayma tutarı çok yüksek ve kötü bir orana neden olacak. Lütfen kayma toleransınızı %15'in altında bir değere düşürün." - }, "swapsMaxSlippage": { "message": "Kayma toleransı" }, - "swapsNotEnoughForTx": { - "message": "Bu işlemi tamamlamak için yeterli $1 yok", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Yeterli $1 yok", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Token ondalığı gereklidir. Şurada bulabilirsiniz: $1" }, - "tokenDecimalTitle": { - "message": "Token ondalığı:" - }, "tokenDetails": { "message": "Token bilgileri" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "Talebi güncelle" }, - "updatedWithDate": { - "message": "$1 güncellendi" - }, "uploadDropFile": { "message": "Dosyanızı buraya sürükleyin" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Üçüncü taraf bilgilerini doğrula" }, - "verifyThisTokenOn": { - "message": "Şurada bu tokeni doğrula: $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Bu tokeni $1 ile doğrulayın ve işlem yapmak istediğiniz tokenin bu olduğundan emin olun.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Sürüm" }, @@ -6229,26 +6064,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..0bac5423d1ee 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "Ẩn thông tin nhạy cảm" }, - "hideToken": { - "message": "Ẩn token" - }, "hideTokenPrompt": { "message": "Ẩn token?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Tập tin JSON", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "Không tìm thấy tài khoản nào cho cụm từ tìm kiếm đã đưa ra" }, - "noConnectedAccountDescription": { - "message": "Chọn tài khoản mà bạn muốn sử dụng trên trang web này để tiếp tục." - }, "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, - "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 +3434,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 +3850,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 +4074,6 @@ "rejected": { "message": "Đã từ chối" }, - "remember": { - "message": "Ghi nhớ:" - }, "remove": { "message": "Xóa" }, @@ -4561,9 +4481,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 +4670,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 +5086,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" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Chúng tôi sẵn sàng cho bạn xem báo giá mới nhất khi bạn muốn tiếp tục" }, - "swapBuildQuotePlaceHolderText": { - "message": "Không có token nào khớp với $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Xác nhận ví cứng của bạn" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Lỗi tìm nạp báo giá" }, - "swapFetchingTokens": { - "message": "Đang tìm nạp token..." - }, "swapFromTo": { "message": "Giao dịch hoán đổi $1 sang $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "Mức trượt giá cao" }, - "swapHighSlippageWarning": { - "message": "Số tiền trượt giá rất cao." - }, "swapIncludesMMFee": { "message": "Bao gồm $1% phí của MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "Mức trượt giá thấp" }, - "swapLowSlippageError": { - "message": "Giao dịch có thể không thành công, mức trượt giá tối đa quá thấp." - }, "swapMaxSlippage": { "message": "Mức trượt giá tối đa" }, @@ -5438,9 +5331,6 @@ "message": "Chênh lệch giá ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Tác động về giá là mức chênh lệch giữa giá thị trường hiện tại và số tiền nhận được trong quá trình thực hiện giao dịch. Tác động giá là một hàm trong quy mô giao dịch của bạn so với quy mô của nhóm thanh khoản." - }, "swapPriceUnavailableDescription": { "message": "Không thể xác định tác động giá do thiếu dữ liệu giá thị trường. Vui lòng xác nhận rằng bạn cảm thấy thoải mái với số lượng token bạn sắp nhận được trước khi hoán đổi." }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Yêu cầu báo giá" }, - "swapReviewSwap": { - "message": "Xem lại giao dịch hoán đổi" - }, - "swapSearchNameOrAddress": { - "message": "Tìm kiếm tên hoặc dán địa chỉ" - }, "swapSelect": { "message": "Chọn" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Mức trượt giá thấp" }, - "swapSlippageNegative": { - "message": "Mức trượt giá phải lớn hơn hoặc bằng 0" - }, "swapSlippageNegativeDescription": { "message": "Mức trượt giá phải lớn hơn hoặc bằng 0" }, @@ -5596,20 +5477,6 @@ "message": "Hoán đổi $1 sang $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Token này đã được thêm theo cách thủ công." - }, - "swapTokenVerificationMessage": { - "message": "Luôn xác nhận địa chỉ token trên $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Chỉ được xác minh trên 1 nguồn." - }, - "swapTokenVerificationSources": { - "message": "Đã xác minh trên $1 nguồn.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 chỉ được xác minh trên 1 nguồn. Hãy xem xét xác minh nó trên $2 trước khi tiếp tục.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "Không xác định" }, - "swapVerifyTokenExplanation": { - "message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Có sẵn $1 $2 để hoán đổi", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "Mức trượt giá 0%" }, - "swapsAdvancedOptions": { - "message": "Tùy chọn nâng cao" - }, - "swapsExcessiveSlippageWarning": { - "message": "Mức trượt giá quá cao và sẽ dẫn đến tỷ giá không sinh lời. Vui lòng giảm giới hạn trượt giá xuống một giá trị thấp hơn 15%." - }, "swapsMaxSlippage": { "message": "Giới hạn trượt giá" }, - "swapsNotEnoughForTx": { - "message": "Không đủ $1 để hoàn thành giao dịch này", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Không đủ $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "Yêu cầu số thập phân của token. Tìm trên: $1" }, - "tokenDecimalTitle": { - "message": "Số thập phân của token:" - }, "tokenDetails": { "message": "Chi tiết token" }, @@ -6091,9 +5937,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" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "Xác minh thông tin bên thứ ba" }, - "verifyThisTokenOn": { - "message": "Xác minh token này trên $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Hãy xác minh token này trên $1 và đảm bảo đây là token bạn muốn giao dịch.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Phiên bản" }, @@ -6229,26 +6064,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..7209fb1c5b44 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" @@ -2114,9 +2063,6 @@ "hideSentitiveInfo": { "message": "隐藏敏感信息" }, - "hideToken": { - "message": "隐藏代币" - }, "hideTokenPrompt": { "message": "隐藏代币?" }, @@ -2442,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON 文件", "description": "format for importing an account" @@ -2851,10 +2794,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" @@ -3092,15 +3031,9 @@ "noAccountsFound": { "message": "未找到符合给定查询条件的账户" }, - "noConnectedAccountDescription": { - "message": "选择要在此站点上使用的账户以继续。" - }, "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, - "noConversionDateAvailable": { - "message": "没有可用的货币转换日期" - }, "noConversionRateAvailable": { "message": "无可用汇率" }, @@ -3501,10 +3434,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 +3850,6 @@ "priceUnavailable": { "message": "价格不可用" }, - "primaryCurrencySetting": { - "message": "主要货币" - }, - "primaryCurrencySettingDescription": { - "message": "选择原生以优先显示链的原生货币(例如 ETH)的值。选择法币以优先显示以您所选法币显示的值。" - }, "primaryType": { "message": "主要类型" }, @@ -4151,9 +4074,6 @@ "rejected": { "message": "已拒绝" }, - "remember": { - "message": "记住:" - }, "remove": { "message": "删除" }, @@ -4561,9 +4481,6 @@ "sepolia": { "message": "Sepolia测试网络" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask 使用这些可信的第三方服务来提高产品可用性和安全性。" - }, "setApprovalForAll": { "message": "设置批准所有" }, @@ -4753,13 +4670,6 @@ "smartTransactionSuccess": { "message": "您的交易已完成" }, - "smartTransactionTakingTooLong": { - "message": "抱歉让您久等" - }, - "smartTransactionTakingTooLongDescription": { - "message": "如果您的交易在 $1 内未完成,则会取消,您无需支付燃料费。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "智能交易" }, @@ -5176,10 +5086,6 @@ "stxCancelledSubDescription": { "message": "再次尝试进行交换。下次我们会在这里保护您免受类似风险。 " }, - "stxEstimatedCompletion": { - "message": "预计将在 $1 内完成", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "交换失败" }, @@ -5286,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "如果您想继续,我们准备好为您显示最新报价" }, - "swapBuildQuotePlaceHolderText": { - "message": "没有与 $1 匹配的代币", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "使用您的硬件钱包确认" }, @@ -5354,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "获取报价出错" }, - "swapFetchingTokens": { - "message": "获取代币中……" - }, "swapFromTo": { "message": "$1 到 $2 的交换", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5377,9 +5276,6 @@ "swapHighSlippage": { "message": "高滑点" }, - "swapHighSlippageWarning": { - "message": "滑点金额非常高。" - }, "swapIncludesMMFee": { "message": "包括 $1% 的 MetaMask 费用。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5401,9 +5297,6 @@ "swapLowSlippage": { "message": "低滑点" }, - "swapLowSlippageError": { - "message": "交易可能会失败,最大滑点过低。" - }, "swapMaxSlippage": { "message": "最大滑点" }, @@ -5438,9 +5331,6 @@ "message": "~$1% 的价差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "价格影响是当前市场价格与交易执行期间收到的金额之间的差异。价格影响是您的交易规模相对于流动性池规模的一个函数。" - }, "swapPriceUnavailableDescription": { "message": "由于缺乏市场价格数据,无法确定价格影响。在交换之前,请确认您对即将收到的代币数量感到满意。" }, @@ -5487,12 +5377,6 @@ "swapRequestForQuotation": { "message": "请求报价" }, - "swapReviewSwap": { - "message": "审查交换" - }, - "swapSearchNameOrAddress": { - "message": "搜索名称或粘贴地址" - }, "swapSelect": { "message": "选择" }, @@ -5525,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "低滑点" }, - "swapSlippageNegative": { - "message": "滑点必须大于或等于0" - }, "swapSlippageNegativeDescription": { "message": "滑点必须大于或等于 0" }, @@ -5596,20 +5477,6 @@ "message": "用 $1 交换 $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "此代币已手动添加。" - }, - "swapTokenVerificationMessage": { - "message": "始终在 $1 上确认代币地址。", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "仅在1个来源上进行了验证。" - }, - "swapTokenVerificationSources": { - "message": "在 $1 个来源上进行了验证。", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 仅在 1 个源上进行了验证。在继续之前,考虑在 $2 上进行验证。", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5630,30 +5497,12 @@ "swapUnknown": { "message": "未知" }, - "swapVerifyTokenExplanation": { - "message": "多个代币可以使用相同的名称和符号。检查 $1 以确认这是您正在寻找的代币。", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 可用于交换", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0%滑点" }, - "swapsAdvancedOptions": { - "message": "高级选项" - }, - "swapsExcessiveSlippageWarning": { - "message": "滑点金额太高,会导致不良率。请将最大滑点降低到低于15%的值。" - }, "swapsMaxSlippage": { "message": "最大滑点" }, - "swapsNotEnoughForTx": { - "message": "没有足够的 $1 来完成此交易", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 不足", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -5789,9 +5638,6 @@ "tokenDecimalFetchFailed": { "message": "需要代币小数位。请在下方查找:$1" }, - "tokenDecimalTitle": { - "message": "代币小数:" - }, "tokenDetails": { "message": "代币详情" }, @@ -6091,9 +5937,6 @@ "updateRequest": { "message": "更新请求" }, - "updatedWithDate": { - "message": "已于 $1 更新" - }, "uploadDropFile": { "message": "将您的文件放在此处" }, @@ -6155,14 +5998,6 @@ "verifyContractDetails": { "message": "验证第三方详情" }, - "verifyThisTokenOn": { - "message": "在 $1 上验证此代币", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "在 $1 上验证此代币,并确保这是您想要交易的代币。", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "版本" }, @@ -6229,26 +6064,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..7cdfa8e28add 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": "隱私政策" }, @@ -1233,9 +1211,6 @@ "supportCenter": { "message": "造訪我們的協助中心" }, - "swapSearchNameOrAddress": { - "message": "用名稱搜尋或貼上地址" - }, "switchEthereumChainConfirmationDescription": { "message": "這將在 MetaMask 中將目前選擇的網路切換到剛才新增的網路:" }, @@ -1380,9 +1355,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 作為開頭" }, @@ -1398,10 +1370,6 @@ "userName": { "message": "使用者名稱" }, - "verifyThisTokenOn": { - "message": "在 $1 驗證這個代幣的資訊", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "查看所有詳情" }, diff --git a/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/images/ape.svg b/app/images/ape.svg new file mode 100644 index 000000000000..495a73676a5e --- /dev/null +++ b/app/images/ape.svg @@ -0,0 +1,1658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json index 31b1c82224fd..f29b7458a9e5 100644 --- a/app/manifest/v2/_base.json +++ b/app/manifest/v2/_base.json @@ -42,7 +42,7 @@ "all_frames": true }, { - "matches": ["*://connect.trezor.io/*/popup.html"], + "matches": ["*://connect.trezor.io/*/popup.html*"], "js": ["vendor/trezor/content-script.js"] } ], diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json index 71d083208b55..4d6ee38437d3 100644 --- a/app/manifest/v3/_base.json +++ b/app/manifest/v3/_base.json @@ -40,7 +40,7 @@ "all_frames": true }, { - "matches": ["*://connect.trezor.io/*/popup.html"], + "matches": ["*://connect.trezor.io/*/popup.html*"], "js": ["vendor/trezor/content-script.js"] } ], diff --git a/app/scripts/background.js b/app/scripts/background.js index 4fbbee449160..9f203b35661d 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -18,7 +18,7 @@ import { isObject } from '@metamask/utils'; import { ApprovalType } from '@metamask/controller-utils'; import PortStream from 'extension-port-stream'; -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import { NotificationServicesController } from '@metamask/notification-services-controller'; @@ -235,7 +235,7 @@ function maybeDetectPhishing(theController) { return {}; } - const prefState = theController.preferencesController.store.getState(); + const prefState = theController.preferencesController.state; if (!prefState.usePhishDetect) { return {}; } @@ -266,13 +266,17 @@ function maybeDetectPhishing(theController) { } theController.phishingController.maybeUpdateState(); - const phishingTestResponse = theController.phishingController.test( - details.url, - ); const blockedRequestResponse = theController.phishingController.isBlockedRequest(details.url); + let phishingTestResponse; + if (details.type === 'main_frame' || details.type === 'sub_frame') { + phishingTestResponse = theController.phishingController.test( + details.url, + ); + } + // if the request is not blocked, and the phishing test is not blocked, return and don't show the phishing screen if (!phishingTestResponse?.result && !blockedRequestResponse.result) { return {}; @@ -758,8 +762,7 @@ export function setupController( controller.preferencesController, ), getUseAddressBarEnsResolution: () => - controller.preferencesController.store.getState() - .useAddressBarEnsResolution, + controller.preferencesController.state.useAddressBarEnsResolution, provider: controller.provider, }); @@ -1156,7 +1159,7 @@ export function setupController( default: controller.approvalController.reject( id, - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ); break; } diff --git a/app/scripts/constants/contracts.ts b/app/scripts/constants/contracts.ts index bc27be31d95c..fae185b1a499 100644 --- a/app/scripts/constants/contracts.ts +++ b/app/scripts/constants/contracts.ts @@ -4,7 +4,7 @@ export const SINGLE_CALL_BALANCES_ADDRESSES = { [CHAIN_IDS.MAINNET]: '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39', [CHAIN_IDS.GOERLI]: '0x9788C4E93f9002a7ad8e72633b11E8d1ecd51f9b', // TODO(SEPOLIA) There is currently no balance call address for Sepolia - [CHAIN_IDS.SEPOLIA]: '', + // [CHAIN_IDS.SEPOLIA]: '', [CHAIN_IDS.BSC]: '0x2352c63A83f9Fd126af8676146721Fa00924d7e4', [CHAIN_IDS.OPTIMISM]: '0xB1c568e9C3E6bdaf755A60c7418C269eb11524FC', [CHAIN_IDS.POLYGON]: '0x2352c63A83f9Fd126af8676146721Fa00924d7e4', 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..7456244fc5a4 --- /dev/null +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -0,0 +1,839 @@ +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 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-controller.ts, 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]; + +async function withController( + ...args: WithControllerArgs +): Promise { + 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: 'selectedNetworkId', + chainId: currentChainId, + }); + + 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 getPreferencesControllerState = jest.fn().mockReturnValue({ + useMultiAccountBalanceChecker, + }); + controllerMessenger.registerActionHandler( + 'PreferencesController:getState', + getPreferencesControllerState, + ); + + const controller = new AccountTrackerController({ + state: getDefaultAccountTrackerControllerState(), + provider: provider as Provider, + blockTracker: blockTrackerStub as unknown as BlockTracker, + getNetworkIdentifier: jest.fn(), + messenger: controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + 'PreferencesController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), + ...accountTrackerOptions, + }); + + return await 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 () => { + await 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 () => { + await 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 () => { + await 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(); + await 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 () => { + await 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 () => { + await 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', async () => { + await withController(({ controller }) => { + expect(() => { + controller.stopPollingByPollingToken(undefined); + }).toThrow('pollingToken required'); + }); + }); + + it('should error if no matching pollingToken is found', async () => { + await 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'); + } + }); + await 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, + }); + await 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() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }; + case 'selectedNetworkClientId': + return { + configuration: { + chainId: currentChainId, + }, + }; + default: + throw new Error('unexpected networkClientId'); + } + }); + await withController( + { + getNetworkClientById: getNetworkClientByIdStub, + }, + 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 () => { + await 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 () => { + await 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 () => { + await withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: true, + state: mockInitialState, + getNetworkClientById: jest.fn().mockReturnValue({ + configuration: { + chainId: '0x999', + }, + }), + }, + 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 () => { + await withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: false, + state: mockInitialState, + getNetworkClientById: jest.fn().mockReturnValue({ + configuration: { + chainId: '0x999', + }, + }), + }, + 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 () => { + await 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: { + [currentChainId]: { ...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: { + [currentChainId]: accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + }); + }); + + describe('onAccountRemoved', () => { + it('should remove an account from state', async () => { + await 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', async () => { + await 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..5f509a1901bf --- /dev/null +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -0,0 +1,834 @@ +/* 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 { PreferencesControllerGetStateAction } 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 + | PreferencesControllerGetStateAction; + +/** + * 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; +}; + +/** + * 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']; + + #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.preferencesControllerState - The state of 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; + + // 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.messagingSystem.call( + 'PreferencesController: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 = (blockNumber: string) => + this.#updateForBlockByNetworkClientId(networkClientId, blockNumber); + 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.messagingSystem.call( + 'PreferencesController: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.messagingSystem.call( + 'PreferencesController: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/alert-controller.test.ts b/app/scripts/controllers/alert-controller.test.ts new file mode 100644 index 000000000000..a8aee606e02d --- /dev/null +++ b/app/scripts/controllers/alert-controller.test.ts @@ -0,0 +1,258 @@ +/** + * @jest-environment node + */ +import { ControllerMessenger } from '@metamask/base-controller'; +import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; +import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; +import { EthAccountType } from '@metamask/keyring-api'; +import { + AlertControllerActions, + AlertControllerEvents, + AlertController, + AllowedActions, + AllowedEvents, + AlertControllerState, +} from './alert-controller'; + +const EMPTY_ACCOUNT = { + id: '', + address: '', + options: {}, + methods: [], + type: EthAccountType.Eoa, + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, + }, +}; +describe('AlertController', () => { + let controllerMessenger: ControllerMessenger< + AlertControllerActions | AllowedActions, + | AlertControllerEvents + | KeyringControllerStateChangeEvent + | SnapControllerStateChangeEvent + | AllowedEvents + >; + let alertController: AlertController; + + beforeEach(() => { + controllerMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + () => EMPTY_ACCOUNT, + ); + + const alertMessenger = controllerMessenger.getRestricted({ + name: 'AlertController', + allowedActions: [`AccountsController:getSelectedAccount`], + allowedEvents: [`AccountsController:selectedAccountChange`], + }); + + alertController = new AlertController({ + state: { + unconnectedAccountAlertShownOrigins: { + testUnconnectedOrigin: false, + }, + web3ShimUsageOrigins: { + testWeb3ShimUsageOrigin: 0, + }, + }, + controllerMessenger: alertMessenger, + }); + }); + + describe('default state', () => { + it('should be same as AlertControllerState initialized', () => { + expect(alertController.store.getState()).toStrictEqual({ + alertEnabledness: { + unconnectedAccount: true, + web3ShimUsage: true, + }, + unconnectedAccountAlertShownOrigins: { + testUnconnectedOrigin: false, + }, + web3ShimUsageOrigins: { + testWeb3ShimUsageOrigin: 0, + }, + }); + }); + }); + + describe('alertEnabledness', () => { + it('should default unconnectedAccount of alertEnabledness to true', () => { + expect( + alertController.store.getState().alertEnabledness.unconnectedAccount, + ).toStrictEqual(true); + }); + + it('should set unconnectedAccount of alertEnabledness to false', () => { + alertController.setAlertEnabledness('unconnectedAccount', false); + expect( + alertController.store.getState().alertEnabledness.unconnectedAccount, + ).toStrictEqual(false); + expect( + controllerMessenger.call('AlertController:getState').alertEnabledness + .unconnectedAccount, + ).toStrictEqual(false); + }); + }); + + describe('unconnectedAccountAlertShownOrigins', () => { + it('should default unconnectedAccountAlertShownOrigins', () => { + expect( + alertController.store.getState().unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: false, + }); + expect( + controllerMessenger.call('AlertController:getState') + .unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: false, + }); + }); + + it('should set unconnectedAccountAlertShownOrigins', () => { + alertController.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); + expect( + alertController.store.getState().unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: true, + }); + expect( + controllerMessenger.call('AlertController:getState') + .unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: true, + }); + }); + }); + + describe('web3ShimUsageOrigins', () => { + it('should default web3ShimUsageOrigins', () => { + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 0, + }); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 0, + }); + }); + + it('should set origin of web3ShimUsageOrigins to recorded', () => { + alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + }); + it('should set origin of web3ShimUsageOrigins to dismissed', () => { + alertController.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 2, + }); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 2, + }); + }); + }); + + describe('selectedAccount change', () => { + it('should set unconnectedAccountAlertShownOrigins to {}', () => { + controllerMessenger.publish('AccountsController:selectedAccountChange', { + id: '', + address: '0x1234567', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, + }, + }); + expect( + alertController.store.getState().unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); + expect( + controllerMessenger.call('AlertController:getState') + .unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); + }); + }); + + describe('AlertController:getState', () => { + it('should return the current state of the property', () => { + const defaultWeb3ShimUsageOrigins = { + testWeb3ShimUsageOrigin: 0, + }; + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual(defaultWeb3ShimUsageOrigins); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual(defaultWeb3ShimUsageOrigins); + }); + }); + + describe('AlertController:stateChange', () => { + it('state will be published when there is state change', () => { + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 0, + }); + + controllerMessenger.subscribe( + 'AlertController:stateChange', + (state: Partial) => { + expect(state.web3ShimUsageOrigins).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + }, + ); + + alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + + expect( + alertController.store.getState().web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + expect( + alertController.getWeb3ShimUsageState('testWeb3ShimUsageOrigin'), + ).toStrictEqual(1); + expect( + controllerMessenger.call('AlertController:getState') + .web3ShimUsageOrigins, + ).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); + }); + }); +}); diff --git a/app/scripts/controllers/alert-controller.ts b/app/scripts/controllers/alert-controller.ts new file mode 100644 index 000000000000..9e1882035e02 --- /dev/null +++ b/app/scripts/controllers/alert-controller.ts @@ -0,0 +1,203 @@ +import { ObservableStore } from '@metamask/obs-store'; +import { + AccountsControllerGetSelectedAccountAction, + AccountsControllerSelectedAccountChangeEvent, +} from '@metamask/accounts-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + TOGGLEABLE_ALERT_TYPES, + Web3ShimUsageAlertStates, +} from '../../../shared/constants/alerts'; + +const controllerName = 'AlertController'; + +/** + * Returns the state of the {@link AlertController}. + */ +export type AlertControllerGetStateAction = { + type: 'AlertController:getState'; + handler: () => AlertControllerState; +}; + +/** + * Actions exposed by the {@link AlertController}. + */ +export type AlertControllerActions = AlertControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AlertController} changes. + */ +export type AlertControllerStateChangeEvent = { + type: 'AlertController:stateChange'; + payload: [AlertControllerState, []]; +}; + +/** + * Events emitted by {@link AlertController}. + */ +export type AlertControllerEvents = AlertControllerStateChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = AccountsControllerGetSelectedAccountAction; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; + +export type AlertControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AlertControllerActions | AllowedActions, + AlertControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +/** + * The alert controller state type + * + * @property alertEnabledness - A map of alerts IDs to booleans, where + * `true` indicates that the alert is enabled and shown, and `false` the opposite. + * @property unconnectedAccountAlertShownOrigins - A map of origin + * strings to booleans indicating whether the "switch to connected" alert has + * been shown (`true`) or otherwise (`false`). + */ +export type AlertControllerState = { + alertEnabledness: Record; + unconnectedAccountAlertShownOrigins: Record; + web3ShimUsageOrigins?: Record; +}; + +/** + * The alert controller options + * + * @property state - The initial controller state + * @property controllerMessenger - The controller messenger + */ +type AlertControllerOptions = { + state?: Partial; + controllerMessenger: AlertControllerMessenger; +}; + +const defaultState: AlertControllerState = { + alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( + (alertEnabledness: Record, alertType: string) => { + alertEnabledness[alertType] = true; + return alertEnabledness; + }, + {}, + ), + unconnectedAccountAlertShownOrigins: {}, + web3ShimUsageOrigins: {}, +}; + +/** + * Controller responsible for maintaining alert-related state. + */ +export class AlertController { + store: ObservableStore; + + readonly #controllerMessenger: AlertControllerMessenger; + + #selectedAddress: string; + + constructor(opts: AlertControllerOptions) { + const state: AlertControllerState = { + ...defaultState, + ...opts.state, + }; + + this.store = new ObservableStore(state); + this.#controllerMessenger = opts.controllerMessenger; + this.#controllerMessenger.registerActionHandler( + 'AlertController:getState', + () => this.store.getState(), + ); + this.store.subscribe((alertState: AlertControllerState) => { + this.#controllerMessenger.publish( + 'AlertController:stateChange', + alertState, + [], + ); + }); + + this.#selectedAddress = this.#controllerMessenger.call( + 'AccountsController:getSelectedAccount', + ).address; + + this.#controllerMessenger.subscribe( + 'AccountsController:selectedAccountChange', + (account: { address: string }) => { + const currentState = this.store.getState(); + if ( + currentState.unconnectedAccountAlertShownOrigins && + this.#selectedAddress !== account.address + ) { + this.#selectedAddress = account.address; + this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); + } + }, + ); + } + + setAlertEnabledness(alertId: string, enabledness: boolean): void { + const { alertEnabledness } = this.store.getState(); + alertEnabledness[alertId] = enabledness; + this.store.updateState({ alertEnabledness }); + } + + /** + * Sets the "switch to connected" alert as shown for the given origin + * + * @param origin - The origin the alert has been shown for + */ + setUnconnectedAccountAlertShown(origin: string): void { + const { unconnectedAccountAlertShownOrigins } = this.store.getState(); + unconnectedAccountAlertShownOrigins[origin] = true; + this.store.updateState({ unconnectedAccountAlertShownOrigins }); + } + + /** + * Gets the web3 shim usage state for the given origin. + * + * @param origin - The origin to get the web3 shim usage state for. + * @returns The web3 shim usage state for the given + * origin, or undefined. + */ + getWeb3ShimUsageState(origin: string): number | undefined { + return this.store.getState().web3ShimUsageOrigins?.[origin]; + } + + /** + * Sets the web3 shim usage state for the given origin to RECORDED. + * + * @param origin - The origin the that used the web3 shim. + */ + setWeb3ShimUsageRecorded(origin: string): void { + this.#setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.recorded); + } + + /** + * Sets the web3 shim usage state for the given origin to DISMISSED. + * + * @param origin - The origin that the web3 shim notification was + * dismissed for. + */ + setWeb3ShimUsageAlertDismissed(origin: string): void { + this.#setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.dismissed); + } + + /** + * @param origin - The origin to set the state for. + * @param value - The state value to set. + */ + #setWeb3ShimUsageState(origin: string, value: number): void { + const { web3ShimUsageOrigins } = this.store.getState(); + if (web3ShimUsageOrigins) { + web3ShimUsageOrigins[origin] = value; + this.store.updateState({ web3ShimUsageOrigins }); + } + } +} diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js deleted file mode 100644 index d13e4cb2fbbf..000000000000 --- a/app/scripts/controllers/alert.js +++ /dev/null @@ -1,136 +0,0 @@ -import { ObservableStore } from '@metamask/obs-store'; -import { - TOGGLEABLE_ALERT_TYPES, - Web3ShimUsageAlertStates, -} from '../../../shared/constants/alerts'; - -/** - * @typedef {object} AlertControllerInitState - * @property {object} alertEnabledness - A map of alerts IDs to booleans, where - * `true` indicates that the alert is enabled and shown, and `false` the opposite. - * @property {object} unconnectedAccountAlertShownOrigins - A map of origin - * strings to booleans indicating whether the "switch to connected" alert has - * been shown (`true`) or otherwise (`false`). - */ - -/** - * @typedef {object} AlertControllerOptions - * @property {AlertControllerInitState} initState - The initial controller state - */ - -const defaultState = { - alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( - (alertEnabledness, alertType) => { - alertEnabledness[alertType] = true; - return alertEnabledness; - }, - {}, - ), - unconnectedAccountAlertShownOrigins: {}, - web3ShimUsageOrigins: {}, -}; - -/** - * Controller responsible for maintaining alert-related state. - */ -export default class AlertController { - /** - * @param {AlertControllerOptions} [opts] - Controller configuration parameters - */ - constructor(opts = {}) { - const { initState = {}, controllerMessenger } = opts; - const state = { - ...defaultState, - alertEnabledness: { - ...defaultState.alertEnabledness, - ...initState.alertEnabledness, - }, - }; - - this.store = new ObservableStore(state); - this.controllerMessenger = controllerMessenger; - - this.selectedAddress = this.controllerMessenger.call( - 'AccountsController:getSelectedAccount', - ); - - this.controllerMessenger.subscribe( - 'AccountsController:selectedAccountChange', - (account) => { - const currentState = this.store.getState(); - if ( - currentState.unconnectedAccountAlertShownOrigins && - this.selectedAddress !== account.address - ) { - this.selectedAddress = account.address; - this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); - } - }, - ); - } - - setAlertEnabledness(alertId, enabledness) { - let { alertEnabledness } = this.store.getState(); - alertEnabledness = { ...alertEnabledness }; - alertEnabledness[alertId] = enabledness; - this.store.updateState({ alertEnabledness }); - } - - /** - * Sets the "switch to connected" alert as shown for the given origin - * - * @param {string} origin - The origin the alert has been shown for - */ - setUnconnectedAccountAlertShown(origin) { - let { unconnectedAccountAlertShownOrigins } = this.store.getState(); - unconnectedAccountAlertShownOrigins = { - ...unconnectedAccountAlertShownOrigins, - }; - unconnectedAccountAlertShownOrigins[origin] = true; - this.store.updateState({ unconnectedAccountAlertShownOrigins }); - } - - /** - * Gets the web3 shim usage state for the given origin. - * - * @param {string} origin - The origin to get the web3 shim usage state for. - * @returns {undefined | 1 | 2} The web3 shim usage state for the given - * origin, or undefined. - */ - getWeb3ShimUsageState(origin) { - return this.store.getState().web3ShimUsageOrigins[origin]; - } - - /** - * Sets the web3 shim usage state for the given origin to RECORDED. - * - * @param {string} origin - The origin the that used the web3 shim. - */ - setWeb3ShimUsageRecorded(origin) { - this._setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.recorded); - } - - /** - * Sets the web3 shim usage state for the given origin to DISMISSED. - * - * @param {string} origin - The origin that the web3 shim notification was - * dismissed for. - */ - setWeb3ShimUsageAlertDismissed(origin) { - this._setWeb3ShimUsageState(origin, Web3ShimUsageAlertStates.dismissed); - } - - /** - * @private - * @param {string} origin - The origin to set the state for. - * @param {number} value - The state value to set. - */ - _setWeb3ShimUsageState(origin, value) { - let { web3ShimUsageOrigins } = this.store.getState(); - web3ShimUsageOrigins = { - ...web3ShimUsageOrigins, - }; - web3ShimUsageOrigins[origin] = value; - this.store.updateState({ web3ShimUsageOrigins }); - } -} diff --git a/app/scripts/controllers/app-state-controller.test.ts b/app/scripts/controllers/app-state-controller.test.ts new file mode 100644 index 000000000000..740c4a7d33f8 --- /dev/null +++ b/app/scripts/controllers/app-state-controller.test.ts @@ -0,0 +1,730 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import { Browser } from 'webextension-polyfill'; +import { + ENVIRONMENT_TYPE_POPUP, + ORIGIN_METAMASK, + POLLING_TOKEN_ENVIRONMENT_TYPES, +} from '../../../shared/constants/app'; +import { AppStateController } from './app-state-controller'; +import type { + AllowedActions, + AllowedEvents, + AppStateControllerActions, + AppStateControllerEvents, + AppStateControllerState, +} from './app-state-controller'; +import { PreferencesControllerState } from './preferences-controller'; + +jest.mock('webextension-polyfill'); + +const mockIsManifestV3 = jest.fn().mockReturnValue(false); +jest.mock('../../../shared/modules/mv3.utils', () => ({ + get isManifestV3() { + return mockIsManifestV3(); + }, +})); + +let appStateController: AppStateController; +let controllerMessenger: ControllerMessenger< + AppStateControllerActions | AllowedActions, + AppStateControllerEvents | AllowedEvents +>; + +const extensionMock = { + alarms: { + getAll: jest.fn(() => Promise.resolve([])), + create: jest.fn(), + clear: jest.fn(), + onAlarm: { + addListener: jest.fn(), + }, + }, +} as unknown as jest.Mocked; + +describe('AppStateController', () => { + const createAppStateController = ( + initState: Partial = {}, + ): { + appStateController: AppStateController; + controllerMessenger: typeof controllerMessenger; + } => { + controllerMessenger = new ControllerMessenger(); + jest.spyOn(ControllerMessenger.prototype, 'call'); + const appStateMessenger = controllerMessenger.getRestricted({ + name: 'AppStateController', + allowedActions: [ + `ApprovalController:addRequest`, + `ApprovalController:acceptRequest`, + `PreferencesController:getState`, + ], + allowedEvents: [ + `PreferencesController:stateChange`, + `KeyringController:qrKeyringStateChange`, + ], + }); + controllerMessenger.registerActionHandler( + 'PreferencesController:getState', + jest.fn().mockReturnValue({ + preferences: { + autoLockTimeLimit: 0, + }, + }), + ); + controllerMessenger.registerActionHandler( + 'ApprovalController:addRequest', + jest.fn().mockReturnValue({ + catch: jest.fn(), + }), + ); + appStateController = new AppStateController({ + addUnlockListener: jest.fn(), + isUnlocked: jest.fn(() => true), + initState, + onInactiveTimeout: jest.fn(), + messenger: appStateMessenger, + extension: extensionMock, + }); + + return { appStateController, controllerMessenger }; + }; + + const createIsUnlockedMock = (isUnlocked: boolean) => { + return jest + .spyOn( + appStateController as unknown as { isUnlocked: () => boolean }, + 'isUnlocked', + ) + .mockReturnValue(isUnlocked); + }; + + beforeEach(() => { + ({ appStateController } = createAppStateController()); + }); + + describe('setOutdatedBrowserWarningLastShown', () => { + it('sets the last shown time', () => { + ({ appStateController } = createAppStateController()); + const timestamp: number = Date.now(); + + appStateController.setOutdatedBrowserWarningLastShown(timestamp); + + expect( + appStateController.store.getState().outdatedBrowserWarningLastShown, + ).toStrictEqual(timestamp); + }); + + it('sets outdated browser warning last shown timestamp', () => { + const lastShownTimestamp: number = Date.now(); + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + outdatedBrowserWarningLastShown: lastShownTimestamp, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('getUnlockPromise', () => { + it('waits for unlock if the extension is locked', async () => { + ({ appStateController } = createAppStateController()); + const isUnlockedMock = createIsUnlockedMock(false); + const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock'); + + appStateController.getUnlockPromise(true); + expect(isUnlockedMock).toHaveBeenCalled(); + expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true); + }); + + it('resolves immediately if the extension is already unlocked', async () => { + ({ appStateController } = createAppStateController()); + const isUnlockedMock = createIsUnlockedMock(true); + + await expect( + appStateController.getUnlockPromise(false), + ).resolves.toBeUndefined(); + + expect(isUnlockedMock).toHaveBeenCalled(); + }); + }); + + describe('waitForUnlock', () => { + it('resolves immediately if already unlocked', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn: () => void = jest.fn(); + appStateController.waitForUnlock(resolveFn, false); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(controllerMessenger.call).toHaveBeenCalledTimes(1); + }); + + it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => { + createIsUnlockedMock(false); + + const resolveFn: () => void = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + expect(controllerMessenger.call).toHaveBeenCalledTimes(2); + expect(controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + expect.objectContaining({ + id: expect.any(String), + origin: ORIGIN_METAMASK, + type: 'unlock', + }), + true, + ); + }); + }); + + describe('handleUnlock', () => { + beforeEach(() => { + createIsUnlockedMock(false); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('accepts approval request revolving all the related promises', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn: () => void = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + appStateController.handleUnlock(); + + expect(emitSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(controllerMessenger.call).toHaveBeenCalled(); + expect(controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + expect.any(String), + ); + }); + }); + + describe('setDefaultHomeActiveTabName', () => { + it('sets the default home tab name', () => { + appStateController.setDefaultHomeActiveTabName('testTabName'); + expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( + 'testTabName', + ); + }); + }); + + describe('setConnectedStatusPopoverHasBeenShown', () => { + it('sets connected status popover as shown', () => { + appStateController.setConnectedStatusPopoverHasBeenShown(); + expect( + appStateController.store.getState().connectedStatusPopoverHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderHasBeenShown', () => { + it('sets recovery phrase reminder as shown', () => { + appStateController.setRecoveryPhraseReminderHasBeenShown(); + expect( + appStateController.store.getState().recoveryPhraseReminderHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderLastShown', () => { + it('sets the last shown time of recovery phrase reminder', () => { + const timestamp: number = Date.now(); + appStateController.setRecoveryPhraseReminderLastShown(timestamp); + + expect( + appStateController.store.getState().recoveryPhraseReminderLastShown, + ).toBe(timestamp); + }); + }); + + describe('setLastActiveTime', () => { + it('sets the last active time to the current time', () => { + const spy = jest.spyOn( + appStateController as unknown as { _resetTimer: () => void }, + '_resetTimer', + ); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + }); + + it('sets the timer if timeoutMinutes is set', () => { + const timeout = Date.now(); + controllerMessenger.publish( + 'PreferencesController:stateChange', + { + preferences: { autoLockTimeLimit: timeout }, + } as unknown as PreferencesControllerState, + [], + ); + const spy = jest.spyOn( + appStateController as unknown as { _resetTimer: () => void }, + '_resetTimer', + ); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('setBrowserEnvironment', () => { + it('sets the current browser and OS environment', () => { + appStateController.setBrowserEnvironment('Windows', 'Chrome'); + expect( + appStateController.store.getState().browserEnvironment, + ).toStrictEqual({ + os: 'Windows', + browser: 'Chrome', + }); + }); + }); + + describe('addPollingToken', () => { + it('adds a pollingToken for a given environmentType', () => { + const pollingTokenType = + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_POPUP]; + appStateController.addPollingToken('token1', pollingTokenType); + expect(appStateController.store.getState()[pollingTokenType]).toContain( + 'token1', + ); + }); + }); + + describe('removePollingToken', () => { + it('removes a pollingToken for a given environmentType', () => { + const pollingTokenType = + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_POPUP]; + appStateController.addPollingToken('token1', pollingTokenType); + appStateController.removePollingToken('token1', pollingTokenType); + expect( + appStateController.store.getState()[pollingTokenType], + ).not.toContain('token1'); + }); + }); + + describe('clearPollingTokens', () => { + it('clears all pollingTokens', () => { + appStateController.addPollingToken('token1', 'popupGasPollTokens'); + appStateController.addPollingToken('token2', 'notificationGasPollTokens'); + appStateController.addPollingToken('token3', 'fullScreenGasPollTokens'); + appStateController.clearPollingTokens(); + + expect( + appStateController.store.getState().popupGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().notificationGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().fullScreenGasPollTokens, + ).toStrictEqual([]); + }); + }); + + describe('setShowTestnetMessageInDropdown', () => { + it('sets whether the testnet dismissal link should be shown in the network dropdown', () => { + appStateController.setShowTestnetMessageInDropdown(true); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(true); + + appStateController.setShowTestnetMessageInDropdown(false); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(false); + }); + }); + + describe('setShowBetaHeader', () => { + it('sets whether the beta notification heading on the home page', () => { + appStateController.setShowBetaHeader(true); + expect(appStateController.store.getState().showBetaHeader).toBe(true); + + appStateController.setShowBetaHeader(false); + expect(appStateController.store.getState().showBetaHeader).toBe(false); + }); + }); + + describe('setCurrentPopupId', () => { + it('sets the currentPopupId in the appState', () => { + const popupId = 12345; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.store.getState().currentPopupId).toBe(popupId); + }); + }); + + describe('getCurrentPopupId', () => { + it('retrieves the currentPopupId saved in the appState', () => { + const popupId = 54321; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.getCurrentPopupId()).toBe(popupId); + }); + }); + + describe('setFirstTimeUsedNetwork', () => { + it('updates the array of the first time used networks', () => { + const chainId = '0x1'; + + appStateController.setFirstTimeUsedNetwork(chainId); + expect(appStateController.store.getState().usedNetworks[chainId]).toBe( + true, + ); + }); + }); + + describe('setLastInteractedConfirmationInfo', () => { + it('sets information about last confirmation user has interacted with', () => { + const lastInteractedConfirmationInfo = { + id: '123', + chainId: '0x1', + timestamp: new Date().getTime(), + }; + appStateController.setLastInteractedConfirmationInfo( + lastInteractedConfirmationInfo, + ); + expect(appStateController.getLastInteractedConfirmationInfo()).toBe( + lastInteractedConfirmationInfo, + ); + + appStateController.setLastInteractedConfirmationInfo(undefined); + expect(appStateController.getLastInteractedConfirmationInfo()).toBe( + undefined, + ); + }); + }); + + describe('setSnapsInstallPrivacyWarningShownStatus', () => { + it('updates the status of snaps install privacy warning', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setSnapsInstallPrivacyWarningShownStatus(true); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + snapsInstallPrivacyWarningShown: true, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('institutional', () => { + it('set the interactive replacement token with a url and the old refresh token', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = { + url: 'https://example.com', + oldRefreshToken: 'old', + }; + + appStateController.showInteractiveReplacementTokenBanner(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + interactiveReplacementToken: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + + it('set the setCustodianDeepLink with the fromAddress and custodyId', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = { + fromAddress: '0x', + custodyId: 'custodyId', + }; + + appStateController.setCustodianDeepLink(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + custodianDeepLink: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + + it('set the setNoteToTraderMessage with a message', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = 'some message'; + + appStateController.setNoteToTraderMessage(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + noteToTraderMessage: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setSurveyLinkLastClickedOrClosed', () => { + it('set the surveyLinkLastClickedOrClosed time', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = Date.now(); + + appStateController.setSurveyLinkLastClickedOrClosed(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + surveyLinkLastClickedOrClosed: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setOnboardingDate', () => { + it('set the onboardingDate', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setOnboardingDate(); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setLastViewedUserSurvey', () => { + it('set the lastViewedUserSurvey with id 1', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = 1; + + appStateController.setLastViewedUserSurvey(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + lastViewedUserSurvey: mockParams, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setNewPrivacyPolicyToastClickedOrClosed', () => { + it('set the newPrivacyPolicyToastClickedOrClosed to true', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setNewPrivacyPolicyToastClickedOrClosed(); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect( + appStateController.store.getState() + .newPrivacyPolicyToastClickedOrClosed, + ).toStrictEqual(true); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setNewPrivacyPolicyToastShownDate', () => { + it('set the newPrivacyPolicyToastShownDate', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = Date.now(); + + appStateController.setNewPrivacyPolicyToastShownDate(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + newPrivacyPolicyToastShownDate: mockParams, + }); + expect( + appStateController.store.getState().newPrivacyPolicyToastShownDate, + ).toStrictEqual(mockParams); + + updateStateSpy.mockRestore(); + }); + }); + + describe('setTermsOfUseLastAgreed', () => { + it('set the termsOfUseLastAgreed timestamp', () => { + ({ appStateController } = createAppStateController()); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + const mockParams = Date.now(); + + appStateController.setTermsOfUseLastAgreed(mockParams); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + termsOfUseLastAgreed: mockParams, + }); + expect( + appStateController.store.getState().termsOfUseLastAgreed, + ).toStrictEqual(mockParams); + + updateStateSpy.mockRestore(); + }); + }); + + describe('onPreferencesStateChange', () => { + it('should update the timeoutMinutes with the autoLockTimeLimit', () => { + ({ appStateController, controllerMessenger } = + createAppStateController()); + const timeout = Date.now(); + + controllerMessenger.publish( + 'PreferencesController:stateChange', + { + preferences: { autoLockTimeLimit: timeout }, + } as unknown as PreferencesControllerState, + [], + ); + + expect(appStateController.store.getState().timeoutMinutes).toStrictEqual( + timeout, + ); + }); + }); + + describe('isManifestV3', () => { + it('creates alarm when isManifestV3 is true', () => { + mockIsManifestV3.mockReturnValue(true); + ({ appStateController } = createAppStateController()); + + const timeout = Date.now(); + controllerMessenger.publish( + 'PreferencesController:stateChange', + { + preferences: { autoLockTimeLimit: timeout }, + } as unknown as PreferencesControllerState, + [], + ); + const spy = jest.spyOn( + appStateController as unknown as { _resetTimer: () => void }, + '_resetTimer', + ); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + expect(extensionMock.alarms.clear).toHaveBeenCalled(); + expect(extensionMock.alarms.onAlarm.addListener).toHaveBeenCalled(); + }); + }); + + describe('AppStateController:getState', () => { + it('should return the current state of the property', () => { + expect( + appStateController.store.getState().recoveryPhraseReminderHasBeenShown, + ).toStrictEqual(false); + expect( + controllerMessenger.call('AppStateController:getState') + .recoveryPhraseReminderHasBeenShown, + ).toStrictEqual(false); + }); + }); + + describe('AppStateController:stateChange', () => { + it('subscribers will recieve the state when published', () => { + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(null); + const timeNow = Date.now(); + controllerMessenger.subscribe( + 'AppStateController:stateChange', + (state: Partial) => { + if (typeof state.surveyLinkLastClickedOrClosed === 'number') { + appStateController.setSurveyLinkLastClickedOrClosed( + state.surveyLinkLastClickedOrClosed, + ); + } + }, + ); + + controllerMessenger.publish( + 'AppStateController:stateChange', + { + surveyLinkLastClickedOrClosed: timeNow, + } as unknown as AppStateControllerState, + [], + ); + + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + expect( + controllerMessenger.call('AppStateController:getState') + .surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + }); + + it('state will be published when there is state change', () => { + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(null); + const timeNow = Date.now(); + controllerMessenger.subscribe( + 'AppStateController:stateChange', + (state: Partial) => { + expect(state.surveyLinkLastClickedOrClosed).toStrictEqual(timeNow); + }, + ); + + appStateController.setSurveyLinkLastClickedOrClosed(timeNow); + + expect( + appStateController.store.getState().surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + expect( + controllerMessenger.call('AppStateController:getState') + .surveyLinkLastClickedOrClosed, + ).toStrictEqual(timeNow); + }); + }); +}); diff --git a/app/scripts/controllers/app-state-controller.ts b/app/scripts/controllers/app-state-controller.ts new file mode 100644 index 000000000000..e76b8fe3888e --- /dev/null +++ b/app/scripts/controllers/app-state-controller.ts @@ -0,0 +1,874 @@ +import EventEmitter from 'events'; +import { ObservableStore } from '@metamask/obs-store'; +import { v4 as uuid } from 'uuid'; +import log from 'loglevel'; +import { ApprovalType } from '@metamask/controller-utils'; +import { KeyringControllerQRKeyringStateChangeEvent } from '@metamask/keyring-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + AcceptRequest, + AddApprovalRequest, +} from '@metamask/approval-controller'; +import { Json } from '@metamask/utils'; +import { Browser } from 'webextension-polyfill'; +import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; +import { MINUTE } from '../../../shared/constants/time'; +import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; +import { isManifestV3 } from '../../../shared/modules/mv3.utils'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { isBeta } from '../../../ui/helpers/utils/build-types'; +import { + ENVIRONMENT_TYPE_BACKGROUND, + POLLING_TOKEN_ENVIRONMENT_TYPES, + ORIGIN_METAMASK, +} from '../../../shared/constants/app'; +import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; +import { LastInteractedConfirmationInfo } from '../../../shared/types/confirm'; +import { SecurityAlertResponse } from '../lib/ppom/types'; +import type { + Preferences, + PreferencesControllerGetStateAction, + PreferencesControllerStateChangeEvent, +} from './preferences-controller'; + +export type AppStateControllerState = { + timeoutMinutes: number; + connectedStatusPopoverHasBeenShown: boolean; + defaultHomeActiveTabName: string | null; + browserEnvironment: Record; + popupGasPollTokens: string[]; + notificationGasPollTokens: string[]; + fullScreenGasPollTokens: string[]; + recoveryPhraseReminderHasBeenShown: boolean; + recoveryPhraseReminderLastShown: number; + outdatedBrowserWarningLastShown: number | null; + nftsDetectionNoticeDismissed: boolean; + showTestnetMessageInDropdown: boolean; + showBetaHeader: boolean; + showPermissionsTour: boolean; + showNetworkBanner: boolean; + showAccountBanner: boolean; + trezorModel: string | null; + currentPopupId?: number; + onboardingDate: number | null; + lastViewedUserSurvey: number | null; + newPrivacyPolicyToastClickedOrClosed: boolean | null; + newPrivacyPolicyToastShownDate: number | null; + // This key is only used for checking if the user had set advancedGasFee + // prior to Migration 92.3 where we split out the setting to support + // multiple networks. + hadAdvancedGasFeesSetPriorToMigration92_3: boolean; + qrHardware: Json; + nftsDropdownState: Json; + usedNetworks: Record; + surveyLinkLastClickedOrClosed: number | null; + signatureSecurityAlertResponses: Record; + // States used for displaying the changed network toast + switchedNetworkDetails: Record | null; + switchedNetworkNeverShowMessage: boolean; + currentExtensionPopupId: number; + lastInteractedConfirmationInfo?: LastInteractedConfirmationInfo; + termsOfUseLastAgreed?: number; + snapsInstallPrivacyWarningShown?: boolean; + interactiveReplacementToken?: { url: string; oldRefreshToken: string }; + noteToTraderMessage?: string; + custodianDeepLink?: { fromAddress: string; custodyId: string }; +}; + +const controllerName = 'AppStateController'; + +/** + * Returns the state of the {@link AppStateController}. + */ +export type AppStateControllerGetStateAction = { + type: 'AppStateController:getState'; + handler: () => AppStateControllerState; +}; + +/** + * Actions exposed by the {@link AppStateController}. + */ +export type AppStateControllerActions = AppStateControllerGetStateAction; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | AddApprovalRequest + | AcceptRequest + | PreferencesControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AppStateController} changes. + */ +export type AppStateControllerStateChangeEvent = { + type: 'AppStateController:stateChange'; + payload: [AppStateControllerState, []]; +}; + +/** + * Events emitted by {@link AppStateController}. + */ +export type AppStateControllerEvents = AppStateControllerStateChangeEvent; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = + | PreferencesControllerStateChangeEvent + | KeyringControllerQRKeyringStateChangeEvent; + +export type AppStateControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AppStateControllerActions | AllowedActions, + AppStateControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +type PollingTokenType = + | 'popupGasPollTokens' + | 'notificationGasPollTokens' + | 'fullScreenGasPollTokens'; + +type AppStateControllerInitState = Partial< + Omit< + AppStateControllerState, + | 'qrHardware' + | 'nftsDropdownState' + | 'usedNetworks' + | 'surveyLinkLastClickedOrClosed' + | 'signatureSecurityAlertResponses' + | 'switchedNetworkDetails' + | 'switchedNetworkNeverShowMessage' + | 'currentExtensionPopupId' + > +>; + +type AppStateControllerOptions = { + addUnlockListener: (callback: () => void) => void; + isUnlocked: () => boolean; + initState?: AppStateControllerInitState; + onInactiveTimeout?: () => void; + messenger: AppStateControllerMessenger; + extension: Browser; +}; + +const getDefaultAppStateControllerState = ( + initState?: AppStateControllerInitState, +): AppStateControllerState => ({ + timeoutMinutes: DEFAULT_AUTO_LOCK_TIME_LIMIT, + connectedStatusPopoverHasBeenShown: true, + defaultHomeActiveTabName: null, + browserEnvironment: {}, + popupGasPollTokens: [], + notificationGasPollTokens: [], + fullScreenGasPollTokens: [], + recoveryPhraseReminderHasBeenShown: false, + recoveryPhraseReminderLastShown: new Date().getTime(), + outdatedBrowserWarningLastShown: null, + nftsDetectionNoticeDismissed: false, + showTestnetMessageInDropdown: true, + showBetaHeader: isBeta(), + showPermissionsTour: true, + showNetworkBanner: true, + showAccountBanner: true, + trezorModel: null, + onboardingDate: null, + lastViewedUserSurvey: null, + newPrivacyPolicyToastClickedOrClosed: null, + newPrivacyPolicyToastShownDate: null, + hadAdvancedGasFeesSetPriorToMigration92_3: false, + ...initState, + qrHardware: {}, + nftsDropdownState: {}, + usedNetworks: { + '0x1': true, + '0x5': true, + '0x539': true, + }, + surveyLinkLastClickedOrClosed: null, + signatureSecurityAlertResponses: {}, + switchedNetworkDetails: null, + switchedNetworkNeverShowMessage: false, + currentExtensionPopupId: 0, +}); + +export class AppStateController extends EventEmitter { + private readonly extension: AppStateControllerOptions['extension']; + + private readonly onInactiveTimeout: () => void; + + store: ObservableStore; + + private timer: NodeJS.Timeout | null; + + isUnlocked: () => boolean; + + private readonly waitingForUnlock: { resolve: () => void }[]; + + private readonly messagingSystem: AppStateControllerMessenger; + + #approvalRequestId: string | null; + + constructor(opts: AppStateControllerOptions) { + const { + addUnlockListener, + isUnlocked, + initState, + onInactiveTimeout, + messenger, + extension, + } = opts; + super(); + + this.extension = extension; + this.onInactiveTimeout = onInactiveTimeout || (() => undefined); + this.store = new ObservableStore( + getDefaultAppStateControllerState(initState), + ); + this.timer = null; + + this.isUnlocked = isUnlocked; + this.waitingForUnlock = []; + addUnlockListener(this.handleUnlock.bind(this)); + + messenger.subscribe( + 'PreferencesController:stateChange', + ({ preferences }: { preferences: Partial }) => { + const currentState = this.store.getState(); + if ( + typeof preferences?.autoLockTimeLimit === 'number' && + currentState.timeoutMinutes !== preferences.autoLockTimeLimit + ) { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + }, + ); + + messenger.subscribe( + 'KeyringController:qrKeyringStateChange', + (qrHardware: Json) => + this.store.updateState({ + qrHardware, + }), + ); + + const { preferences } = messenger.call('PreferencesController:getState'); + if (typeof preferences.autoLockTimeLimit === 'number') { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + + this.messagingSystem = messenger; + this.messagingSystem.registerActionHandler( + 'AppStateController:getState', + () => this.store.getState(), + ); + this.store.subscribe((state: AppStateControllerState) => { + this.messagingSystem.publish('AppStateController:stateChange', state, []); + }); + this.#approvalRequestId = null; + } + + /** + * Get a Promise that resolves when the extension is unlocked. + * This Promise will never reject. + * + * @param shouldShowUnlockRequest - Whether the extension notification + * popup should be opened. + * @returns A promise that resolves when the extension is + * unlocked, or immediately if the extension is already unlocked. + */ + getUnlockPromise(shouldShowUnlockRequest: boolean): Promise { + return new Promise((resolve) => { + if (this.isUnlocked()) { + resolve(); + } else { + this.waitForUnlock(resolve, shouldShowUnlockRequest); + } + }); + } + + /** + * Adds a Promise's resolve function to the waitingForUnlock queue. + * Also opens the extension popup if specified. + * + * @param resolve - A Promise's resolve function that will + * be called when the extension is unlocked. + * @param shouldShowUnlockRequest - Whether the extension notification + * popup should be opened. + */ + waitForUnlock(resolve: () => void, shouldShowUnlockRequest: boolean): void { + this.waitingForUnlock.push({ resolve }); + this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); + if (shouldShowUnlockRequest) { + this._requestApproval(); + } + } + + /** + * Drains the waitingForUnlock queue, resolving all the related Promises. + */ + handleUnlock(): void { + if (this.waitingForUnlock.length > 0) { + while (this.waitingForUnlock.length > 0) { + this.waitingForUnlock.shift()?.resolve(); + } + this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); + } + + this._acceptApproval(); + } + + /** + * Sets the default home tab + * + * @param defaultHomeActiveTabName - the tab name + */ + setDefaultHomeActiveTabName(defaultHomeActiveTabName: string | null): void { + this.store.updateState({ + defaultHomeActiveTabName, + }); + } + + /** + * Record that the user has seen the connected status info popover + */ + setConnectedStatusPopoverHasBeenShown(): void { + this.store.updateState({ + connectedStatusPopoverHasBeenShown: true, + }); + } + + /** + * Record that the user has been shown the recovery phrase reminder. + */ + setRecoveryPhraseReminderHasBeenShown(): void { + this.store.updateState({ + recoveryPhraseReminderHasBeenShown: true, + }); + } + + setSurveyLinkLastClickedOrClosed(time: number): void { + this.store.updateState({ + surveyLinkLastClickedOrClosed: time, + }); + } + + setOnboardingDate(): void { + this.store.updateState({ + onboardingDate: Date.now(), + }); + } + + setLastViewedUserSurvey(id: number) { + this.store.updateState({ + lastViewedUserSurvey: id, + }); + } + + setNewPrivacyPolicyToastClickedOrClosed(): void { + this.store.updateState({ + newPrivacyPolicyToastClickedOrClosed: true, + }); + } + + setNewPrivacyPolicyToastShownDate(time: number): void { + this.store.updateState({ + newPrivacyPolicyToastShownDate: time, + }); + } + + /** + * Record the timestamp of the last time the user has seen the recovery phrase reminder + * + * @param lastShown - timestamp when user was last shown the reminder. + */ + setRecoveryPhraseReminderLastShown(lastShown: number): void { + this.store.updateState({ + recoveryPhraseReminderLastShown: lastShown, + }); + } + + /** + * Record the timestamp of the last time the user has acceoted the terms of use + * + * @param lastAgreed - timestamp when user last accepted the terms of use + */ + setTermsOfUseLastAgreed(lastAgreed: number): void { + this.store.updateState({ + termsOfUseLastAgreed: lastAgreed, + }); + } + + /** + * Record if popover for snaps privacy warning has been shown + * on the first install of a snap. + * + * @param shown - shown status + */ + setSnapsInstallPrivacyWarningShownStatus(shown: boolean): void { + this.store.updateState({ + snapsInstallPrivacyWarningShown: shown, + }); + } + + /** + * Record the timestamp of the last time the user has seen the outdated browser warning + * + * @param lastShown - Timestamp (in milliseconds) of when the user was last shown the warning. + */ + setOutdatedBrowserWarningLastShown(lastShown: number): void { + this.store.updateState({ + outdatedBrowserWarningLastShown: lastShown, + }); + } + + /** + * Sets the last active time to the current time. + */ + setLastActiveTime(): void { + this._resetTimer(); + } + + /** + * Sets the inactive timeout for the app + * + * @param timeoutMinutes - The inactive timeout in minutes. + */ + private _setInactiveTimeout(timeoutMinutes: number): void { + this.store.updateState({ + timeoutMinutes, + }); + + this._resetTimer(); + } + + /** + * Resets the internal inactive timer + * + * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new + * timer will not be created. + * + */ + private _resetTimer(): void { + const { timeoutMinutes } = this.store.getState(); + + if (this.timer) { + clearTimeout(this.timer); + } else if (isManifestV3) { + this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); + } + + if (!timeoutMinutes) { + return; + } + + // This is a temporary fix until we add a state migration. + // Due to a bug in ui/pages/settings/advanced-tab/advanced-tab.component.js, + // it was possible for timeoutMinutes to be saved as a string, as explained + // in PR 25109. `alarms.create` will fail in that case. We are + // converting this to a number here to prevent that failure. Once + // we add a migration to update the malformed state to the right type, + // we will remove this conversion. + const timeoutToSet = Number(timeoutMinutes); + + if (isManifestV3) { + this.extension.alarms.create(AUTO_LOCK_TIMEOUT_ALARM, { + delayInMinutes: timeoutToSet, + periodInMinutes: timeoutToSet, + }); + this.extension.alarms.onAlarm.addListener( + (alarmInfo: { name: string }) => { + if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) { + this.onInactiveTimeout(); + this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); + } + }, + ); + } else { + this.timer = setTimeout( + () => this.onInactiveTimeout(), + timeoutToSet * MINUTE, + ); + } + } + + /** + * Sets the current browser and OS environment + * + * @param os + * @param browser + */ + setBrowserEnvironment(os: string, browser: string): void { + this.store.updateState({ browserEnvironment: { os, browser } }); + } + + /** + * Adds a pollingToken for a given environmentType + * + * @param pollingToken + * @param pollingTokenType + */ + addPollingToken( + pollingToken: string, + pollingTokenType: PollingTokenType, + ): void { + if ( + pollingTokenType.toString() !== + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] + ) { + if (this.#isValidPollingTokenType(pollingTokenType)) { + this.#updatePollingTokens(pollingToken, pollingTokenType); + } + } + } + + /** + * Updates the polling token in the state. + * + * @param pollingToken + * @param pollingTokenType + */ + #updatePollingTokens( + pollingToken: string, + pollingTokenType: PollingTokenType, + ) { + const currentTokens: string[] = this.store.getState()[pollingTokenType]; + this.store.updateState({ + [pollingTokenType]: [...currentTokens, pollingToken], + }); + } + + /** + * removes a pollingToken for a given environmentType + * + * @param pollingToken + * @param pollingTokenType + */ + removePollingToken( + pollingToken: string, + pollingTokenType: PollingTokenType, + ): void { + if ( + pollingTokenType.toString() !== + POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] + ) { + const currentTokens: string[] = this.store.getState()[pollingTokenType]; + if (this.#isValidPollingTokenType(pollingTokenType)) { + this.store.updateState({ + [pollingTokenType]: currentTokens.filter( + (token: string) => token !== pollingToken, + ), + }); + } + } + } + + /** + * Validates whether the given polling token type is a valid one. + * + * @param pollingTokenType + * @returns true if valid, false otherwise. + */ + #isValidPollingTokenType(pollingTokenType: PollingTokenType): boolean { + const validTokenTypes: PollingTokenType[] = [ + 'popupGasPollTokens', + 'notificationGasPollTokens', + 'fullScreenGasPollTokens', + ]; + + return validTokenTypes.includes(pollingTokenType); + } + + /** + * clears all pollingTokens + */ + clearPollingTokens(): void { + this.store.updateState({ + popupGasPollTokens: [], + notificationGasPollTokens: [], + fullScreenGasPollTokens: [], + }); + } + + /** + * Sets whether the testnet dismissal link should be shown in the network dropdown + * + * @param showTestnetMessageInDropdown + */ + setShowTestnetMessageInDropdown(showTestnetMessageInDropdown: boolean): void { + this.store.updateState({ showTestnetMessageInDropdown }); + } + + /** + * Sets whether the beta notification heading on the home page + * + * @param showBetaHeader + */ + setShowBetaHeader(showBetaHeader: boolean): void { + this.store.updateState({ showBetaHeader }); + } + + /** + * Sets whether the permissions tour should be shown to the user + * + * @param showPermissionsTour + */ + setShowPermissionsTour(showPermissionsTour: boolean): void { + this.store.updateState({ showPermissionsTour }); + } + + /** + * Sets whether the Network Banner should be shown + * + * @param showNetworkBanner + */ + setShowNetworkBanner(showNetworkBanner: boolean): void { + this.store.updateState({ showNetworkBanner }); + } + + /** + * Sets whether the Account Banner should be shown + * + * @param showAccountBanner + */ + setShowAccountBanner(showAccountBanner: boolean): void { + this.store.updateState({ showAccountBanner }); + } + + /** + * Sets a unique ID for the current extension popup + * + * @param currentExtensionPopupId + */ + setCurrentExtensionPopupId(currentExtensionPopupId: number): void { + this.store.updateState({ currentExtensionPopupId }); + } + + /** + * Sets an object with networkName and appName + * or `null` if the message is meant to be cleared + * + * @param switchedNetworkDetails - Details about the network that MetaMask just switched to. + */ + setSwitchedNetworkDetails( + switchedNetworkDetails: { origin: string; networkClientId: string } | null, + ): void { + this.store.updateState({ switchedNetworkDetails }); + } + + /** + * Clears the switched network details in state + */ + clearSwitchedNetworkDetails(): void { + this.store.updateState({ switchedNetworkDetails: null }); + } + + /** + * Remembers if the user prefers to never see the + * network switched message again + * + * @param switchedNetworkNeverShowMessage + */ + setSwitchedNetworkNeverShowMessage( + switchedNetworkNeverShowMessage: boolean, + ): void { + this.store.updateState({ + switchedNetworkDetails: null, + switchedNetworkNeverShowMessage, + }); + } + + /** + * Sets a property indicating the model of the user's Trezor hardware wallet + * + * @param trezorModel - The Trezor model. + */ + setTrezorModel(trezorModel: string | null): void { + this.store.updateState({ trezorModel }); + } + + /** + * A setter for the `nftsDropdownState` property + * + * @param nftsDropdownState + */ + updateNftDropDownState(nftsDropdownState: Json): void { + this.store.updateState({ + nftsDropdownState, + }); + } + + /** + * Updates the array of the first time used networks + * + * @param chainId + */ + setFirstTimeUsedNetwork(chainId: string): void { + const currentState = this.store.getState(); + const { usedNetworks } = currentState; + usedNetworks[chainId] = true; + + this.store.updateState({ usedNetworks }); + } + + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + /** + * Set the interactive replacement token with a url and the old refresh token + * + * @param opts + * @param opts.url + * @param opts.oldRefreshToken + */ + showInteractiveReplacementTokenBanner({ + url, + oldRefreshToken, + }: { + url: string; + oldRefreshToken: string; + }): void { + this.store.updateState({ + interactiveReplacementToken: { + url, + oldRefreshToken, + }, + }); + } + + /** + * Set the setCustodianDeepLink with the fromAddress and custodyId + * + * @param opts + * @param opts.fromAddress + * @param opts.custodyId + */ + setCustodianDeepLink({ + fromAddress, + custodyId, + }: { + fromAddress: string; + custodyId: string; + }): void { + this.store.updateState({ + custodianDeepLink: { fromAddress, custodyId }, + }); + } + + setNoteToTraderMessage(message: string): void { + this.store.updateState({ + noteToTraderMessage: message, + }); + } + + ///: END:ONLY_INCLUDE_IF + + getSignatureSecurityAlertResponse( + securityAlertId: string, + ): SecurityAlertResponse { + return this.store.getState().signatureSecurityAlertResponses[ + securityAlertId + ]; + } + + addSignatureSecurityAlertResponse( + securityAlertResponse: SecurityAlertResponse, + ): void { + const currentState = this.store.getState(); + const { signatureSecurityAlertResponses } = currentState; + if (securityAlertResponse.securityAlertId) { + this.store.updateState({ + signatureSecurityAlertResponses: { + ...signatureSecurityAlertResponses, + [String(securityAlertResponse.securityAlertId)]: + securityAlertResponse, + }, + }); + } + } + + /** + * A setter for the currentPopupId which indicates the id of popup window that's currently active + * + * @param currentPopupId + */ + setCurrentPopupId(currentPopupId: number): void { + this.store.updateState({ + currentPopupId, + }); + } + + /** + * The function returns information about the last confirmation user interacted with + */ + getLastInteractedConfirmationInfo(): + | LastInteractedConfirmationInfo + | undefined { + return this.store.getState().lastInteractedConfirmationInfo; + } + + /** + * Update the information about the last confirmation user interacted with + * + * @param lastInteractedConfirmationInfo + */ + setLastInteractedConfirmationInfo( + lastInteractedConfirmationInfo: LastInteractedConfirmationInfo | undefined, + ): void { + this.store.updateState({ + lastInteractedConfirmationInfo, + }); + } + + /** + * A getter to retrieve currentPopupId saved in the appState + */ + getCurrentPopupId(): number | undefined { + return this.store.getState().currentPopupId; + } + + private _requestApproval(): void { + // If we already have a pending request this is a no-op + if (this.#approvalRequestId) { + return; + } + this.#approvalRequestId = uuid(); + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id: this.#approvalRequestId, + origin: ORIGIN_METAMASK, + type: ApprovalType.Unlock, + }, + true, + ) + .catch(() => { + // If the promise fails, we allow a new popup to be triggered + this.#approvalRequestId = null; + }); + } + + // Override emit method to provide strong typing for events + emit(event: string) { + return super.emit(event); + } + + private _acceptApproval(): void { + if (!this.#approvalRequestId) { + return; + } + try { + this.messagingSystem.call( + 'ApprovalController:acceptRequest', + this.#approvalRequestId, + ); + } catch (error) { + log.error('Failed to unlock approval request', error); + } + + this.#approvalRequestId = null; + } +} diff --git a/app/scripts/controllers/app-state.d.ts b/app/scripts/controllers/app-state.d.ts deleted file mode 100644 index aa7ffc92eb3c..000000000000 --- a/app/scripts/controllers/app-state.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SecurityAlertResponse } from '../lib/ppom/types'; - -export type AppStateController = { - addSignatureSecurityAlertResponse( - securityAlertResponse: SecurityAlertResponse, - ): void; - getUnlockPromise(shouldShowUnlockRequest: boolean): Promise; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - setCustodianDeepLink({ - fromAddress, - custodyId, - }: { - fromAddress: string; - custodyId: string; - }): void; - showInteractiveReplacementTokenBanner({ - oldRefreshToken, - url, - }: { - oldRefreshToken: string; - url: string; - }): void; - ///: END:ONLY_INCLUDE_IF -}; diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js deleted file mode 100644 index 32498a1c8bce..000000000000 --- a/app/scripts/controllers/app-state.js +++ /dev/null @@ -1,637 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { v4 as uuid } from 'uuid'; -import log from 'loglevel'; -import { ApprovalType } from '@metamask/controller-utils'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import { MINUTE } from '../../../shared/constants/time'; -import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; -import { isManifestV3 } from '../../../shared/modules/mv3.utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { isBeta } from '../../../ui/helpers/utils/build-types'; -import { - ENVIRONMENT_TYPE_BACKGROUND, - POLLING_TOKEN_ENVIRONMENT_TYPES, - ORIGIN_METAMASK, -} from '../../../shared/constants/app'; -import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; - -/** @typedef {import('../../../shared/types/confirm').LastInteractedConfirmationInfo} LastInteractedConfirmationInfo */ - -export default class AppStateController extends EventEmitter { - /** - * @param {object} opts - */ - constructor(opts = {}) { - const { - addUnlockListener, - isUnlocked, - initState, - onInactiveTimeout, - preferencesStore, - messenger, - extension, - } = opts; - super(); - - this.extension = extension; - this.onInactiveTimeout = onInactiveTimeout || (() => undefined); - this.store = new ObservableStore({ - timeoutMinutes: DEFAULT_AUTO_LOCK_TIME_LIMIT, - connectedStatusPopoverHasBeenShown: true, - defaultHomeActiveTabName: null, - browserEnvironment: {}, - popupGasPollTokens: [], - notificationGasPollTokens: [], - fullScreenGasPollTokens: [], - recoveryPhraseReminderHasBeenShown: false, - recoveryPhraseReminderLastShown: new Date().getTime(), - outdatedBrowserWarningLastShown: null, - nftsDetectionNoticeDismissed: false, - showTestnetMessageInDropdown: true, - showBetaHeader: isBeta(), - showPermissionsTour: true, - showNetworkBanner: true, - showAccountBanner: true, - trezorModel: null, - currentPopupId: undefined, - onboardingDate: null, - newPrivacyPolicyToastClickedOrClosed: null, - newPrivacyPolicyToastShownDate: null, - // This key is only used for checking if the user had set advancedGasFee - // prior to Migration 92.3 where we split out the setting to support - // multiple networks. - hadAdvancedGasFeesSetPriorToMigration92_3: false, - ...initState, - qrHardware: {}, - nftsDropdownState: {}, - usedNetworks: { - '0x1': true, - '0x5': true, - '0x539': true, - }, - surveyLinkLastClickedOrClosed: null, - signatureSecurityAlertResponses: {}, - // States used for displaying the changed network toast - switchedNetworkDetails: null, - switchedNetworkNeverShowMessage: false, - currentExtensionPopupId: 0, - lastInteractedConfirmationInfo: undefined, - }); - this.timer = null; - - this.isUnlocked = isUnlocked; - this.waitingForUnlock = []; - addUnlockListener(this.handleUnlock.bind(this)); - - preferencesStore.subscribe(({ preferences }) => { - const currentState = this.store.getState(); - if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { - this._setInactiveTimeout(preferences.autoLockTimeLimit); - } - }); - - messenger.subscribe( - 'KeyringController:qrKeyringStateChange', - (qrHardware) => - this.store.updateState({ - qrHardware, - }), - ); - - const { preferences } = preferencesStore.getState(); - this._setInactiveTimeout(preferences.autoLockTimeLimit); - - this.messagingSystem = messenger; - this._approvalRequestId = null; - } - - /** - * Get a Promise that resolves when the extension is unlocked. - * This Promise will never reject. - * - * @param {boolean} shouldShowUnlockRequest - Whether the extension notification - * popup should be opened. - * @returns {Promise} A promise that resolves when the extension is - * unlocked, or immediately if the extension is already unlocked. - */ - getUnlockPromise(shouldShowUnlockRequest) { - return new Promise((resolve) => { - if (this.isUnlocked()) { - resolve(); - } else { - this.waitForUnlock(resolve, shouldShowUnlockRequest); - } - }); - } - - /** - * Adds a Promise's resolve function to the waitingForUnlock queue. - * Also opens the extension popup if specified. - * - * @param {Promise.resolve} resolve - A Promise's resolve function that will - * be called when the extension is unlocked. - * @param {boolean} shouldShowUnlockRequest - Whether the extension notification - * popup should be opened. - */ - waitForUnlock(resolve, shouldShowUnlockRequest) { - this.waitingForUnlock.push({ resolve }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - if (shouldShowUnlockRequest) { - this._requestApproval(); - } - } - - /** - * Drains the waitingForUnlock queue, resolving all the related Promises. - */ - handleUnlock() { - if (this.waitingForUnlock.length > 0) { - while (this.waitingForUnlock.length > 0) { - this.waitingForUnlock.shift().resolve(); - } - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } - - this._acceptApproval(); - } - - /** - * Sets the default home tab - * - * @param {string} [defaultHomeActiveTabName] - the tab name - */ - setDefaultHomeActiveTabName(defaultHomeActiveTabName) { - this.store.updateState({ - defaultHomeActiveTabName, - }); - } - - /** - * Record that the user has seen the connected status info popover - */ - setConnectedStatusPopoverHasBeenShown() { - this.store.updateState({ - connectedStatusPopoverHasBeenShown: true, - }); - } - - /** - * Record that the user has been shown the recovery phrase reminder. - */ - setRecoveryPhraseReminderHasBeenShown() { - this.store.updateState({ - recoveryPhraseReminderHasBeenShown: true, - }); - } - - setSurveyLinkLastClickedOrClosed(time) { - this.store.updateState({ - surveyLinkLastClickedOrClosed: time, - }); - } - - setOnboardingDate() { - this.store.updateState({ - onboardingDate: Date.now(), - }); - } - - setNewPrivacyPolicyToastClickedOrClosed() { - this.store.updateState({ - newPrivacyPolicyToastClickedOrClosed: true, - }); - } - - setNewPrivacyPolicyToastShownDate(time) { - this.store.updateState({ - newPrivacyPolicyToastShownDate: time, - }); - } - - /** - * Record the timestamp of the last time the user has seen the recovery phrase reminder - * - * @param {number} lastShown - timestamp when user was last shown the reminder. - */ - setRecoveryPhraseReminderLastShown(lastShown) { - this.store.updateState({ - recoveryPhraseReminderLastShown: lastShown, - }); - } - - /** - * Record the timestamp of the last time the user has acceoted the terms of use - * - * @param {number} lastAgreed - timestamp when user last accepted the terms of use - */ - setTermsOfUseLastAgreed(lastAgreed) { - this.store.updateState({ - termsOfUseLastAgreed: lastAgreed, - }); - } - - /** - * Record if popover for snaps privacy warning has been shown - * on the first install of a snap. - * - * @param {boolean} shown - shown status - */ - setSnapsInstallPrivacyWarningShownStatus(shown) { - this.store.updateState({ - snapsInstallPrivacyWarningShown: shown, - }); - } - - /** - * Record the timestamp of the last time the user has seen the outdated browser warning - * - * @param {number} lastShown - Timestamp (in milliseconds) of when the user was last shown the warning. - */ - setOutdatedBrowserWarningLastShown(lastShown) { - this.store.updateState({ - outdatedBrowserWarningLastShown: lastShown, - }); - } - - /** - * Sets the last active time to the current time. - */ - setLastActiveTime() { - this._resetTimer(); - } - - /** - * Sets the inactive timeout for the app - * - * @private - * @param {number} timeoutMinutes - The inactive timeout in minutes. - */ - _setInactiveTimeout(timeoutMinutes) { - this.store.updateState({ - timeoutMinutes, - }); - - this._resetTimer(); - } - - /** - * Resets the internal inactive timer - * - * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new - * timer will not be created. - * - * @private - */ - /* eslint-disable no-undef */ - _resetTimer() { - const { timeoutMinutes } = this.store.getState(); - - if (this.timer) { - clearTimeout(this.timer); - } else if (isManifestV3) { - this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); - } - - if (!timeoutMinutes) { - return; - } - - // This is a temporary fix until we add a state migration. - // Due to a bug in ui/pages/settings/advanced-tab/advanced-tab.component.js, - // it was possible for timeoutMinutes to be saved as a string, as explained - // in PR 25109. `alarms.create` will fail in that case. We are - // converting this to a number here to prevent that failure. Once - // we add a migration to update the malformed state to the right type, - // we will remove this conversion. - const timeoutToSet = Number(timeoutMinutes); - - if (isManifestV3) { - this.extension.alarms.create(AUTO_LOCK_TIMEOUT_ALARM, { - delayInMinutes: timeoutToSet, - periodInMinutes: timeoutToSet, - }); - this.extension.alarms.onAlarm.addListener((alarmInfo) => { - if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) { - this.onInactiveTimeout(); - this.extension.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM); - } - }); - } else { - this.timer = setTimeout( - () => this.onInactiveTimeout(), - timeoutToSet * MINUTE, - ); - } - } - - /** - * Sets the current browser and OS environment - * - * @param os - * @param browser - */ - setBrowserEnvironment(os, browser) { - this.store.updateState({ browserEnvironment: { os, browser } }); - } - - /** - * Adds a pollingToken for a given environmentType - * - * @param pollingToken - * @param pollingTokenType - */ - addPollingToken(pollingToken, pollingTokenType) { - if ( - pollingTokenType !== - POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] - ) { - const prevState = this.store.getState()[pollingTokenType]; - this.store.updateState({ - [pollingTokenType]: [...prevState, pollingToken], - }); - } - } - - /** - * removes a pollingToken for a given environmentType - * - * @param pollingToken - * @param pollingTokenType - */ - removePollingToken(pollingToken, pollingTokenType) { - if ( - pollingTokenType !== - POLLING_TOKEN_ENVIRONMENT_TYPES[ENVIRONMENT_TYPE_BACKGROUND] - ) { - const prevState = this.store.getState()[pollingTokenType]; - this.store.updateState({ - [pollingTokenType]: prevState.filter((token) => token !== pollingToken), - }); - } - } - - /** - * clears all pollingTokens - */ - clearPollingTokens() { - this.store.updateState({ - popupGasPollTokens: [], - notificationGasPollTokens: [], - fullScreenGasPollTokens: [], - }); - } - - /** - * Sets whether the testnet dismissal link should be shown in the network dropdown - * - * @param showTestnetMessageInDropdown - */ - setShowTestnetMessageInDropdown(showTestnetMessageInDropdown) { - this.store.updateState({ showTestnetMessageInDropdown }); - } - - /** - * Sets whether the beta notification heading on the home page - * - * @param showBetaHeader - */ - setShowBetaHeader(showBetaHeader) { - this.store.updateState({ showBetaHeader }); - } - - /** - * Sets whether the permissions tour should be shown to the user - * - * @param showPermissionsTour - */ - setShowPermissionsTour(showPermissionsTour) { - this.store.updateState({ showPermissionsTour }); - } - - /** - * Sets whether the Network Banner should be shown - * - * @param showNetworkBanner - */ - setShowNetworkBanner(showNetworkBanner) { - this.store.updateState({ showNetworkBanner }); - } - - /** - * Sets whether the Account Banner should be shown - * - * @param showAccountBanner - */ - setShowAccountBanner(showAccountBanner) { - this.store.updateState({ showAccountBanner }); - } - - /** - * Sets a unique ID for the current extension popup - * - * @param currentExtensionPopupId - */ - setCurrentExtensionPopupId(currentExtensionPopupId) { - this.store.updateState({ currentExtensionPopupId }); - } - - /** - * Sets an object with networkName and appName - * or `null` if the message is meant to be cleared - * - * @param {{ origin: string, networkClientId: string } | null} switchedNetworkDetails - Details about the network that MetaMask just switched to. - */ - setSwitchedNetworkDetails(switchedNetworkDetails) { - this.store.updateState({ switchedNetworkDetails }); - } - - /** - * Clears the switched network details in state - */ - clearSwitchedNetworkDetails() { - this.store.updateState({ switchedNetworkDetails: null }); - } - - /** - * Remembers if the user prefers to never see the - * network switched message again - * - * @param {boolean} switchedNetworkNeverShowMessage - */ - setSwitchedNetworkNeverShowMessage(switchedNetworkNeverShowMessage) { - this.store.updateState({ - switchedNetworkDetails: null, - switchedNetworkNeverShowMessage, - }); - } - - /** - * Sets a property indicating the model of the user's Trezor hardware wallet - * - * @param trezorModel - The Trezor model. - */ - setTrezorModel(trezorModel) { - this.store.updateState({ trezorModel }); - } - - /** - * A setter for the `nftsDropdownState` property - * - * @param nftsDropdownState - */ - updateNftDropDownState(nftsDropdownState) { - this.store.updateState({ - nftsDropdownState, - }); - } - - /** - * Updates the array of the first time used networks - * - * @param chainId - * @returns {void} - */ - setFirstTimeUsedNetwork(chainId) { - const currentState = this.store.getState(); - const { usedNetworks } = currentState; - usedNetworks[chainId] = true; - - this.store.updateState({ usedNetworks }); - } - - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - /** - * Set the interactive replacement token with a url and the old refresh token - * - * @param {object} opts - * @param opts.url - * @param opts.oldRefreshToken - * @returns {void} - */ - showInteractiveReplacementTokenBanner({ url, oldRefreshToken }) { - this.store.updateState({ - interactiveReplacementToken: { - url, - oldRefreshToken, - }, - }); - } - - /** - * Set the setCustodianDeepLink with the fromAddress and custodyId - * - * @param {object} opts - * @param opts.fromAddress - * @param opts.custodyId - * @returns {void} - */ - setCustodianDeepLink({ fromAddress, custodyId }) { - this.store.updateState({ - custodianDeepLink: { fromAddress, custodyId }, - }); - } - - setNoteToTraderMessage(message) { - this.store.updateState({ - noteToTraderMessage: message, - }); - } - - ///: END:ONLY_INCLUDE_IF - - getSignatureSecurityAlertResponse(securityAlertId) { - return this.store.getState().signatureSecurityAlertResponses[ - securityAlertId - ]; - } - - addSignatureSecurityAlertResponse(securityAlertResponse) { - const currentState = this.store.getState(); - const { signatureSecurityAlertResponses } = currentState; - this.store.updateState({ - signatureSecurityAlertResponses: { - ...signatureSecurityAlertResponses, - [securityAlertResponse.securityAlertId]: securityAlertResponse, - }, - }); - } - - /** - * A setter for the currentPopupId which indicates the id of popup window that's currently active - * - * @param currentPopupId - */ - setCurrentPopupId(currentPopupId) { - this.store.updateState({ - currentPopupId, - }); - } - - /** - * The function returns information about the last confirmation user interacted with - * - * @type {LastInteractedConfirmationInfo}: Information about the last confirmation user interacted with. - */ - getLastInteractedConfirmationInfo() { - return this.store.getState().lastInteractedConfirmationInfo; - } - - /** - * Update the information about the last confirmation user interacted with - * - * @type {LastInteractedConfirmationInfo} - information about transaction user last interacted with. - */ - setLastInteractedConfirmationInfo(lastInteractedConfirmationInfo) { - this.store.updateState({ - lastInteractedConfirmationInfo, - }); - } - - /** - * A getter to retrieve currentPopupId saved in the appState - */ - getCurrentPopupId() { - return this.store.getState().currentPopupId; - } - - _requestApproval() { - // If we already have a pending request this is a no-op - if (this._approvalRequestId) { - return; - } - this._approvalRequestId = uuid(); - - this.messagingSystem - .call( - 'ApprovalController:addRequest', - { - id: this._approvalRequestId, - origin: ORIGIN_METAMASK, - type: ApprovalType.Unlock, - }, - true, - ) - .catch(() => { - // If the promise fails, we allow a new popup to be triggered - this._approvalRequestId = null; - }); - } - - _acceptApproval() { - if (!this._approvalRequestId) { - return; - } - try { - this.messagingSystem.call( - 'ApprovalController:acceptRequest', - this._approvalRequestId, - ); - } catch (error) { - log.error('Failed to unlock approval request', error); - } - - this._approvalRequestId = null; - } -} diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js deleted file mode 100644 index c9ce8243b05c..000000000000 --- a/app/scripts/controllers/app-state.test.js +++ /dev/null @@ -1,397 +0,0 @@ -import { ObservableStore } from '@metamask/obs-store'; -import { ORIGIN_METAMASK } from '../../../shared/constants/app'; -import AppStateController from './app-state'; - -let appStateController, mockStore; - -describe('AppStateController', () => { - mockStore = new ObservableStore(); - const createAppStateController = (initState = {}) => { - return new AppStateController({ - addUnlockListener: jest.fn(), - isUnlocked: jest.fn(() => true), - initState, - onInactiveTimeout: jest.fn(), - showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ - preferences: { - autoLockTimeLimit: 0, - }, - })), - }, - messenger: { - call: jest.fn(() => ({ - catch: jest.fn(), - })), - subscribe: jest.fn(), - }, - }); - }; - - beforeEach(() => { - appStateController = createAppStateController({ store: mockStore }); - }); - - describe('setOutdatedBrowserWarningLastShown', () => { - it('sets the last shown time', () => { - appStateController = createAppStateController(); - const date = new Date(); - - appStateController.setOutdatedBrowserWarningLastShown(date); - - expect( - appStateController.store.getState().outdatedBrowserWarningLastShown, - ).toStrictEqual(date); - }); - - it('sets outdated browser warning last shown timestamp', () => { - const lastShownTimestamp = Date.now(); - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - outdatedBrowserWarningLastShown: lastShownTimestamp, - }); - - updateStateSpy.mockRestore(); - }); - }); - - describe('getUnlockPromise', () => { - it('waits for unlock if the extension is locked', async () => { - appStateController = createAppStateController(); - const isUnlockedMock = jest - .spyOn(appStateController, 'isUnlocked') - .mockReturnValue(false); - const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock'); - - appStateController.getUnlockPromise(true); - expect(isUnlockedMock).toHaveBeenCalled(); - expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true); - }); - - it('resolves immediately if the extension is already unlocked', async () => { - appStateController = createAppStateController(); - const isUnlockedMock = jest - .spyOn(appStateController, 'isUnlocked') - .mockReturnValue(true); - - await expect( - appStateController.getUnlockPromise(false), - ).resolves.toBeUndefined(); - - expect(isUnlockedMock).toHaveBeenCalled(); - }); - }); - - describe('waitForUnlock', () => { - it('resolves immediately if already unlocked', async () => { - const emitSpy = jest.spyOn(appStateController, 'emit'); - const resolveFn = jest.fn(); - appStateController.waitForUnlock(resolveFn, false); - expect(emitSpy).toHaveBeenCalledWith('updateBadge'); - expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0); - }); - - it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => { - jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); - - const resolveFn = jest.fn(); - appStateController.waitForUnlock(resolveFn, true); - - expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(1); - expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( - 'ApprovalController:addRequest', - expect.objectContaining({ - id: expect.any(String), - origin: ORIGIN_METAMASK, - type: 'unlock', - }), - true, - ); - }); - }); - - describe('handleUnlock', () => { - beforeEach(() => { - jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('accepts approval request revolving all the related promises', async () => { - const emitSpy = jest.spyOn(appStateController, 'emit'); - const resolveFn = jest.fn(); - appStateController.waitForUnlock(resolveFn, true); - - appStateController.handleUnlock(); - - expect(emitSpy).toHaveBeenCalled(); - expect(emitSpy).toHaveBeenCalledWith('updateBadge'); - expect(appStateController.messagingSystem.call).toHaveBeenCalled(); - expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( - 'ApprovalController:acceptRequest', - expect.any(String), - ); - }); - }); - - describe('setDefaultHomeActiveTabName', () => { - it('sets the default home tab name', () => { - appStateController.setDefaultHomeActiveTabName('testTabName'); - expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( - 'testTabName', - ); - }); - }); - - describe('setConnectedStatusPopoverHasBeenShown', () => { - it('sets connected status popover as shown', () => { - appStateController.setConnectedStatusPopoverHasBeenShown(); - expect( - appStateController.store.getState().connectedStatusPopoverHasBeenShown, - ).toBe(true); - }); - }); - - describe('setRecoveryPhraseReminderHasBeenShown', () => { - it('sets recovery phrase reminder as shown', () => { - appStateController.setRecoveryPhraseReminderHasBeenShown(); - expect( - appStateController.store.getState().recoveryPhraseReminderHasBeenShown, - ).toBe(true); - }); - }); - - describe('setRecoveryPhraseReminderLastShown', () => { - it('sets the last shown time of recovery phrase reminder', () => { - const timestamp = Date.now(); - appStateController.setRecoveryPhraseReminderLastShown(timestamp); - - expect( - appStateController.store.getState().recoveryPhraseReminderLastShown, - ).toBe(timestamp); - }); - }); - - describe('setLastActiveTime', () => { - it('sets the last active time to the current time', () => { - const spy = jest.spyOn(appStateController, '_resetTimer'); - appStateController.setLastActiveTime(); - - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('setBrowserEnvironment', () => { - it('sets the current browser and OS environment', () => { - appStateController.setBrowserEnvironment('Windows', 'Chrome'); - expect( - appStateController.store.getState().browserEnvironment, - ).toStrictEqual({ - os: 'Windows', - browser: 'Chrome', - }); - }); - }); - - describe('addPollingToken', () => { - it('adds a pollingToken for a given environmentType', () => { - const pollingTokenType = 'popupGasPollTokens'; - appStateController.addPollingToken('token1', pollingTokenType); - expect(appStateController.store.getState()[pollingTokenType]).toContain( - 'token1', - ); - }); - }); - - describe('removePollingToken', () => { - it('removes a pollingToken for a given environmentType', () => { - const pollingTokenType = 'popupGasPollTokens'; - appStateController.addPollingToken('token1', pollingTokenType); - appStateController.removePollingToken('token1', pollingTokenType); - expect( - appStateController.store.getState()[pollingTokenType], - ).not.toContain('token1'); - }); - }); - - describe('clearPollingTokens', () => { - it('clears all pollingTokens', () => { - appStateController.addPollingToken('token1', 'popupGasPollTokens'); - appStateController.addPollingToken('token2', 'notificationGasPollTokens'); - appStateController.addPollingToken('token3', 'fullScreenGasPollTokens'); - appStateController.clearPollingTokens(); - - expect( - appStateController.store.getState().popupGasPollTokens, - ).toStrictEqual([]); - expect( - appStateController.store.getState().notificationGasPollTokens, - ).toStrictEqual([]); - expect( - appStateController.store.getState().fullScreenGasPollTokens, - ).toStrictEqual([]); - }); - }); - - describe('setShowTestnetMessageInDropdown', () => { - it('sets whether the testnet dismissal link should be shown in the network dropdown', () => { - appStateController.setShowTestnetMessageInDropdown(true); - expect( - appStateController.store.getState().showTestnetMessageInDropdown, - ).toBe(true); - - appStateController.setShowTestnetMessageInDropdown(false); - expect( - appStateController.store.getState().showTestnetMessageInDropdown, - ).toBe(false); - }); - }); - - describe('setShowBetaHeader', () => { - it('sets whether the beta notification heading on the home page', () => { - appStateController.setShowBetaHeader(true); - expect(appStateController.store.getState().showBetaHeader).toBe(true); - - appStateController.setShowBetaHeader(false); - expect(appStateController.store.getState().showBetaHeader).toBe(false); - }); - }); - - describe('setCurrentPopupId', () => { - it('sets the currentPopupId in the appState', () => { - const popupId = 'popup1'; - - appStateController.setCurrentPopupId(popupId); - expect(appStateController.store.getState().currentPopupId).toBe(popupId); - }); - }); - - describe('getCurrentPopupId', () => { - it('retrieves the currentPopupId saved in the appState', () => { - const popupId = 'popup1'; - - appStateController.setCurrentPopupId(popupId); - expect(appStateController.getCurrentPopupId()).toBe(popupId); - }); - }); - - describe('setFirstTimeUsedNetwork', () => { - it('updates the array of the first time used networks', () => { - const chainId = '0x1'; - - appStateController.setFirstTimeUsedNetwork(chainId); - expect(appStateController.store.getState().usedNetworks[chainId]).toBe( - true, - ); - }); - }); - - describe('setLastInteractedConfirmationInfo', () => { - it('sets information about last confirmation user has interacted with', () => { - const lastInteractedConfirmationInfo = { - id: '123', - chainId: '0x1', - timestamp: new Date().getTime(), - }; - appStateController.setLastInteractedConfirmationInfo( - lastInteractedConfirmationInfo, - ); - expect(appStateController.getLastInteractedConfirmationInfo()).toBe( - lastInteractedConfirmationInfo, - ); - - appStateController.setLastInteractedConfirmationInfo(undefined); - expect(appStateController.getLastInteractedConfirmationInfo()).toBe( - undefined, - ); - }); - }); - - describe('setSnapsInstallPrivacyWarningShownStatus', () => { - it('updates the status of snaps install privacy warning', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - appStateController.setSnapsInstallPrivacyWarningShownStatus(true); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - snapsInstallPrivacyWarningShown: true, - }); - - updateStateSpy.mockRestore(); - }); - }); - - describe('institutional', () => { - it('set the interactive replacement token with a url and the old refresh token', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - const mockParams = { url: 'https://example.com', oldRefreshToken: 'old' }; - - appStateController.showInteractiveReplacementTokenBanner(mockParams); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - interactiveReplacementToken: mockParams, - }); - - updateStateSpy.mockRestore(); - }); - - it('set the setCustodianDeepLink with the fromAddress and custodyId', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - const mockParams = { fromAddress: '0x', custodyId: 'custodyId' }; - - appStateController.setCustodianDeepLink(mockParams); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - custodianDeepLink: mockParams, - }); - - updateStateSpy.mockRestore(); - }); - - it('set the setNoteToTraderMessage with a message', () => { - appStateController = createAppStateController(); - const updateStateSpy = jest.spyOn( - appStateController.store, - 'updateState', - ); - - const mockParams = 'some message'; - - appStateController.setNoteToTraderMessage(mockParams); - - expect(updateStateSpy).toHaveBeenCalledTimes(1); - expect(updateStateSpy).toHaveBeenCalledWith({ - noteToTraderMessage: mockParams, - }); - - updateStateSpy.mockRestore(); - }); - }); -}); 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..aa5546ef7899 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 = { @@ -114,8 +118,9 @@ export default class MetaMetricsController { * @param {object} options * @param {object} options.segment - an instance of analytics for tracking * events that conform to the new MetaMetrics tracking plan. - * @param {object} options.preferencesStore - The preferences controller store, used - * to access and subscribe to preferences that will be attached to events + * @param {object} options.preferencesControllerState - The state of preferences controller + * @param {Function} options.onPreferencesStateChange - Used to attach a listener to the + * stateChange event emitted by the PreferencesController * @param {Function} options.onNetworkDidChange - Used to attach a listener to the * networkDidChange event emitted by the networkController * @param {Function} options.getCurrentChainId - Gets the current chain id from the @@ -128,7 +133,8 @@ export default class MetaMetricsController { */ constructor({ segment, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, onNetworkDidChange, getCurrentChainId, version, @@ -144,16 +150,15 @@ export default class MetaMetricsController { captureException(err); } }; - const prefState = preferencesStore.getState(); this.chainId = getCurrentChainId(); - this.locale = prefState.currentLocale.replace('_', '-'); + this.locale = preferencesControllerState.currentLocale.replace('_', '-'); this.version = environment === 'production' ? version : `${version}-${environment}`; this.extension = extension; this.environment = environment; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - this.selectedAddress = prefState.selectedAddress; + this.selectedAddress = preferencesControllerState.selectedAddress; ///: END:ONLY_INCLUDE_IF const abandonedFragments = omitBy(initState?.fragments, 'persist'); @@ -177,8 +182,8 @@ export default class MetaMetricsController { }, }); - preferencesStore.subscribe(({ currentLocale }) => { - this.locale = currentLocale.replace('_', '-'); + onPreferencesStateChange(({ currentLocale }) => { + this.locale = currentLocale?.replace('_', '-'); }); onNetworkDidChange(() => { @@ -484,8 +489,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 +853,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 +869,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..ca5602de33c8 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -74,22 +74,6 @@ const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, }; -function getMockPreferencesStore({ currentLocale = LOCALE } = {}) { - let preferencesStore = { - currentLocale, - }; - const subscribe = jest.fn(); - const updateState = (newState) => { - preferencesStore = { ...preferencesStore, ...newState }; - subscribe.mock.calls[0][0](preferencesStore); - }; - return { - getState: jest.fn().mockReturnValue(preferencesStore), - updateState, - subscribe, - }; -} - const SAMPLE_PERSISTED_EVENT = { id: 'testid', persist: true, @@ -117,7 +101,10 @@ function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, marketingCampaignCookieId = null, - preferencesStore = getMockPreferencesStore(), + preferencesControllerState = { currentLocale: LOCALE }, + onPreferencesStateChange = () => { + // do nothing + }, getCurrentChainId = () => FAKE_CHAIN_ID, onNetworkDidChange = () => { // do nothing @@ -128,7 +115,8 @@ function getMetaMetricsController({ segment: segmentInstance || segment, getCurrentChainId, onNetworkDidChange, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, version: '0.0.1', environment: 'test', initState: { @@ -209,11 +197,16 @@ describe('MetaMetricsController', function () { }); it('should update when preferences changes', function () { - const preferencesStore = getMockPreferencesStore(); + let subscribeListener; + const onPreferencesStateChange = (listener) => { + subscribeListener = listener; + }; const metaMetricsController = getMetaMetricsController({ - preferencesStore, + preferencesControllerState: { currentLocale: LOCALE }, + onPreferencesStateChange, }); - preferencesStore.updateState({ currentLocale: 'en_UK' }); + + subscribeListener({ currentLocale: 'en_UK' }); expect(metaMetricsController.locale).toStrictEqual('en-UK'); }); }); @@ -732,9 +725,11 @@ describe('MetaMetricsController', function () { it('should track a page view if isOptInPath is true and user not yet opted in', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -746,6 +741,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith( { @@ -765,9 +761,11 @@ describe('MetaMetricsController', function () { it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -790,6 +788,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith( { @@ -1088,7 +1087,7 @@ describe('MetaMetricsController', function () { securityAlertsEnabled: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, security_providers: [], names: { [NameType.ETHEREUM_ADDRESS]: { @@ -1122,6 +1121,11 @@ describe('MetaMetricsController', function () { }, }, }, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, }); expect(traits).toStrictEqual({ @@ -1143,7 +1147,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 +1157,7 @@ describe('MetaMetricsController', function () { ///: BEGIN:ONLY_INCLUDE_IF(petnames) [MetaMetricsUserTrait.PetnameAddressCount]: 3, ///: END:ONLY_INCLUDE_IF + [MetaMetricsUserTrait.TokenSortPreference]: 'token-sort-key', }); }); @@ -1181,7 +1186,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 +1218,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 +1231,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.NumberOfAccounts]: 3, [MetaMetricsUserTrait.NumberOfTokens]: 1, [MetaMetricsUserTrait.OpenseaApiEnabled]: false, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: false, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: false, }); }); @@ -1245,7 +1260,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 +1287,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..7fb87c6d143b 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -18,12 +18,13 @@ import { TEST_NETWORK_TICKER_MAP, } from '../../../shared/constants/network'; import MMIController from './mmi-controller'; -import AppStateController from './app-state'; +import { AppStateController } from './app-state-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../test/stub/networks'; +import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ handleMmiPortfolio: jest.fn(), @@ -99,7 +100,7 @@ describe('MMIController', function () { 'NetworkController:infuraIsUnblocked', ], }), - state: mockNetworkState({chainId: CHAIN_IDS.SEPOLIA}), + state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), infuraProjectId: 'mock-infura-project-id', }); @@ -203,10 +204,10 @@ describe('MMIController', function () { }); metaMetricsController = new MetaMetricsController({ - preferencesStore: { - getState: jest.fn().mockReturnValue({ currentLocale: 'en' }), - subscribe: jest.fn(), + preferencesControllerState: { + currentLocale: 'en' }, + onPreferencesStateChange: jest.fn(), getCurrentChainId: jest.fn(), onNetworkDidChange: jest.fn(), }); @@ -245,15 +246,14 @@ describe('MMIController', function () { initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + messenger: { + ...mockMessenger, + call: jest.fn().mockReturnValue({ preferences: { autoLockTimeLimit: 0, }, - })), - }, - messenger: mockMessenger, + }) + } }), networkController, permissionController, @@ -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, @@ -354,6 +354,33 @@ describe('MMIController', function () { mmiController.mmiConfigurationController.storeConfiguration, ).toHaveBeenCalled(); }); + + it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => { + const mockKeyring = { + on: jest.fn(), + getAccounts: jest.fn().mockResolvedValue([]), + getSupportedChains: jest.fn().mockResolvedValue({}), + }; + + mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring); + mmiController.custodyController.getAllCustodyTypes = jest.fn().mockReturnValue(['mock-custody-type']); + mmiController.logAndStoreApiRequest = jest.fn(); + + await mmiController.onSubmitPassword(); + + expect(mockKeyring.on).toHaveBeenCalledWith( + API_REQUEST_LOG_EVENT, + expect.any(Function) + ); + + const mockLogData = { someKey: 'someValue' }; + const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find( + call => call[0] === API_REQUEST_LOG_EVENT + )[1]; + apiRequestLogEventHandler(mockLogData); + + expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData); + }); }); describe('connectCustodyAddresses', () => { @@ -385,7 +412,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,13 +427,63 @@ 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, ).toHaveBeenCalled(); expect(result).toEqual(['0x1']); }); + + it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => { + const custodianType = 'mock-custodian-type'; + const custodianName = 'mock-custodian-name'; + const accounts = { + '0x1': { + name: 'Account 1', + custodianDetails: {}, + labels: [], + token: 'token', + chainId: 1, + }, + }; + CUSTODIAN_TYPES['MOCK-CUSTODIAN-TYPE'] = { + keyringClass: { type: 'mock-keyring-class' }, + }; + + const mockKeyring = { + on: jest.fn(), + setSelectedAddresses: jest.fn(), + addAccounts: jest.fn(), + getStatusMap: jest.fn(), + }; + + mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring); + mmiController.keyringController.getAccounts = jest.fn().mockResolvedValue(['0x2']); + mmiController.keyringController.addNewAccountForKeyring = jest.fn().mockResolvedValue('0x3'); + mmiController.custodyController.setAccountDetails = jest.fn(); + mmiController.accountTrackerController.syncWithAddresses = jest.fn(); + mmiController.storeCustodianSupportedChains = jest.fn(); + mmiController.custodyController.storeCustodyStatusMap = jest.fn(); + mmiController.logAndStoreApiRequest = jest.fn(); + + await mmiController.connectCustodyAddresses(custodianType, custodianName, accounts); + + expect(mockKeyring.on).toHaveBeenCalledWith( + API_REQUEST_LOG_EVENT, + expect.any(Function) + ); + + const mockLogData = { someKey: 'someValue' }; + const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find( + call => call[0] === API_REQUEST_LOG_EVENT + )[1]; + apiRequestLogEventHandler(mockLogData); + + expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData); + }); }); describe('getCustodianAccounts', () => { @@ -782,4 +859,27 @@ describe('MMIController', function () { ).toHaveBeenCalledWith('/new-account/connect'); }); }); + + describe('logAndStoreApiRequest', () => { + it('should call custodyController.sanitizeAndLogApiCall with the provided log data', async () => { + const mockLogData = { someKey: 'someValue' }; + const mockSanitizedLogs = { sanitizedKey: 'sanitizedValue' }; + + mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockResolvedValue(mockSanitizedLogs); + + const result = await mmiController.logAndStoreApiRequest(mockLogData); + + expect(mmiController.custodyController.sanitizeAndLogApiCall).toHaveBeenCalledWith(mockLogData); + expect(result).toEqual(mockSanitizedLogs); + }); + + it('should handle errors and throw them', async () => { + const mockLogData = { someKey: 'someValue' }; + const mockError = new Error('Sanitize error'); + + mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockRejectedValue(mockError); + + await expect(mmiController.logAndStoreApiRequest(mockLogData)).rejects.toThrow('Sanitize error'); + }); + }); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index cbb08308ec59..65cdac69ba0b 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -13,17 +13,20 @@ import { import { REFRESH_TOKEN_CHANGE_EVENT, INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, + API_REQUEST_LOG_EVENT, } from '@metamask-institutional/sdk'; import { handleMmiPortfolio } from '@metamask-institutional/portfolio-dashboard'; -import { TransactionMeta } from '@metamask/transaction-controller'; -import { KeyringTypes } from '@metamask/keyring-controller'; import { CustodyController } from '@metamask-institutional/custody-controller'; +import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; -import { SignatureController } from '@metamask/signature-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; import { - OriginalRequest, - PersonalMessageParams, -} from '@metamask/message-manager'; + MessageParamsPersonal, + MessageParamsTyped, + SignatureController, +} from '@metamask/signature-controller'; +import { OriginalRequest } from '@metamask/message-manager'; import { NetworkController } from '@metamask/network-controller'; import { InternalAccount } from '@metamask/keyring-api'; import { toHex } from '@metamask/controller-utils'; @@ -39,14 +42,14 @@ 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 PreferencesController from './preferences-controller'; -import { AppStateController } from './app-state'; +import AccountTrackerController from './account-tracker-controller'; +import { AppStateController } from './app-state-controller'; +import { PreferencesController } from './preferences-controller'; type UpdateCustodianTransactionsParameters = { keyring: CustodyKeyring; @@ -86,7 +89,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 +151,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; @@ -304,6 +307,10 @@ export default class MMIController extends EventEmitter { }, ); + keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => { + this.logAndStoreApiRequest(logData); + }); + // store the supported chains for this custodian type const accounts = await keyring.getAccounts(); addresses = addresses.concat(...accounts); @@ -419,6 +426,10 @@ export default class MMIController extends EventEmitter { }, ); + keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => { + this.logAndStoreApiRequest(logData); + }); + if (!keyring) { throw new Error('Unable to get keyring'); } @@ -458,7 +469,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 +515,7 @@ export default class MMIController extends EventEmitter { } }); - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); for (const address of newAccounts) { try { @@ -795,14 +806,14 @@ export default class MMIController extends EventEmitter { req.method === 'eth_signTypedData_v4' ) { return await this.signatureController.newUnsignedTypedMessage( - updatedMsgParams as PersonalMessageParams, + updatedMsgParams as MessageParamsTyped, req as OriginalRequest, version, { parseJsonData: false }, ); } else if (req.method === 'personal_sign') { return await this.signatureController.newUnsignedPersonalMessage( - updatedMsgParams as PersonalMessageParams, + updatedMsgParams as MessageParamsPersonal, req as OriginalRequest, ); } @@ -884,4 +895,14 @@ export default class MMIController extends EventEmitter { this.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE); return true; } + + async logAndStoreApiRequest(logData: IApiCallLogEntry) { + try { + const logs = await this.custodyController.sanitizeAndLogApiCall(logData); + return logs; + } catch (error) { + log.error('Error fetching extension request logs:', error); + throw error; + } + } } 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..fffc9ae44f49 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.`, @@ -409,6 +413,7 @@ export const unrestrictedMethods = Object.freeze([ 'snap_updateInterface', 'snap_getInterfaceState', 'snap_resolveInterface', + 'snap_getCurrencyRate', ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 'metamaskinstitutional_authenticate', 'metamaskinstitutional_reauthenticate', diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index f825c1eb5aee..9c28ed7c43a0 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -3,13 +3,7 @@ */ import { ControllerMessenger } from '@metamask/base-controller'; import { AccountsController } from '@metamask/accounts-controller'; -import { - KeyringControllerGetAccountsAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetKeyringForAccountAction, - KeyringControllerStateChangeEvent, - KeyringControllerAccountRemovedEvent, -} from '@metamask/keyring-controller'; +import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { Hex } from '@metamask/utils'; import { CHAIN_IDS } from '../../../shared/constants/network'; @@ -18,10 +12,10 @@ import { ThemeType } from '../../../shared/constants/preferences'; import type { AllowedActions, AllowedEvents, - PreferencesControllerActions, - PreferencesControllerEvents, + PreferencesControllerMessenger, + PreferencesControllerState, } from './preferences-controller'; -import PreferencesController from './preferences-controller'; +import { PreferencesController } from './preferences-controller'; const NETWORK_CONFIGURATION_DATA = mockNetworkState( { @@ -40,102 +34,104 @@ const NETWORK_CONFIGURATION_DATA = mockNetworkState( }, ).networkConfigurationsByChainId; -describe('preferences controller', () => { - let controllerMessenger: ControllerMessenger< - | PreferencesControllerActions - | AllowedActions - | KeyringControllerGetAccountsAction - | KeyringControllerGetKeyringsByTypeAction - | KeyringControllerGetKeyringForAccountAction, - | PreferencesControllerEvents +const setupController = ({ + state, +}: { + state?: Partial; +}) => { + const controllerMessenger = new ControllerMessenger< + AllowedActions, + | AllowedEvents | KeyringControllerStateChangeEvent - | KeyringControllerAccountRemovedEvent | SnapControllerStateChangeEvent - | AllowedEvents - >; - let preferencesController: PreferencesController; - let accountsController: AccountsController; - - beforeEach(() => { - controllerMessenger = new ControllerMessenger(); - - const accountsControllerMessenger = controllerMessenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); - - const mockAccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }; - accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: mockAccountsControllerState, - }); - - const preferencesMessenger = controllerMessenger.getRestricted({ + >(); + const preferencesControllerMessenger: PreferencesControllerMessenger = + controllerMessenger.getRestricted({ name: 'PreferencesController', allowedActions: [ - `AccountsController:setSelectedAccount`, - `AccountsController:getAccountByAddress`, - `AccountsController:setAccountName`, + 'AccountsController:getAccountByAddress', + 'AccountsController:setAccountName', + 'AccountsController:getSelectedAccount', + 'AccountsController:setSelectedAccount', + 'NetworkController:getState', ], - allowedEvents: [`AccountsController:stateChange`], + allowedEvents: ['AccountsController:stateChange'], }); - preferencesController = new PreferencesController({ - initLangCode: 'en_US', + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue({ networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - messenger: preferencesMessenger, - }); + }), + ); + const controller = new PreferencesController({ + messenger: preferencesControllerMessenger, + state, + }); + + const accountsControllerMessenger = controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'KeyringController:stateChange', + 'SnapController:stateChange', + ], + allowedActions: [], }); + const mockAccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; + const accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: mockAccountsControllerState, + }); + + return { + controller, + messenger: controllerMessenger, + accountsController, + }; +}; +describe('preferences controller', () => { describe('useBlockie', () => { it('defaults useBlockie to false', () => { - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - false, - ); + const { controller } = setupController({}); + expect(controller.state.useBlockie).toStrictEqual(false); }); it('setUseBlockie to true', () => { - preferencesController.setUseBlockie(true); - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - true, - ); + const { controller } = setupController({}); + controller.setUseBlockie(true); + expect(controller.state.useBlockie).toStrictEqual(true); }); }); describe('setCurrentLocale', () => { it('checks the default currentLocale', () => { - const { currentLocale } = preferencesController.store.getState(); - expect(currentLocale).toStrictEqual('en_US'); + const { controller } = setupController({}); + const { currentLocale } = controller.state; + expect(currentLocale).toStrictEqual(''); }); it('sets current locale in preferences controller', () => { - preferencesController.setCurrentLocale('ja'); - const { currentLocale } = preferencesController.store.getState(); + const { controller } = setupController({}); + controller.setCurrentLocale('ja'); + const { currentLocale } = controller.state; expect(currentLocale).toStrictEqual('ja'); }); }); describe('setAccountLabel', () => { + const { controller, messenger, accountsController } = setupController({}); const mockName = 'mockName'; const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; it('updating name from preference controller will update the name in accounts controller and preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -150,21 +146,20 @@ describe('preferences controller', () => { ); let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; expect(firstAccount.metadata.name).toBe(firstPreferenceAccount.name); expect(secondAccount.metadata.name).toBe(secondPreferenceAccount.name); - preferencesController.setAccountLabel(firstAccount.address, mockName); + controller.setAccountLabel(firstAccount.address, mockName); // refresh state after state changed [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -181,7 +176,7 @@ describe('preferences controller', () => { }); it('updating name from accounts controller updates the name in preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -197,7 +192,7 @@ describe('preferences controller', () => { let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; @@ -210,8 +205,7 @@ describe('preferences controller', () => { [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -229,10 +223,11 @@ describe('preferences controller', () => { }); describe('setSelectedAddress', () => { + const { controller, messenger, accountsController } = setupController({}); it('updating selectedAddress from preferences controller updates the selectedAccount in accounts controller and preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -248,25 +243,26 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); - preferencesController.setSelectedAddress(secondAddress); + controller.setSelectedAddress(secondAddress); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); expect(updatedSelectedAddress).toBe(updatedSelectedAccount.address); + + expect(controller.getSelectedAddress()).toBe(secondAddress); }); it('updating selectedAccount from accounts controller updates the selectedAddress in preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -283,15 +279,14 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); const accounts = accountsController.listAccounts(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); accountsController.setSelectedAccount(accounts[1].id); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); @@ -300,173 +295,142 @@ describe('preferences controller', () => { }); describe('setPasswordForgotten', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(false); + expect(controller.state.forgottenPassword).toStrictEqual(false); }); it('should set the forgottenPassword property in state', () => { - preferencesController.setPasswordForgotten(true); - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(true); + controller.setPasswordForgotten(true); + expect(controller.state.forgottenPassword).toStrictEqual(true); }); }); describe('setUsePhishDetect', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); }); it('should set the usePhishDetect property in state', () => { - preferencesController.setUsePhishDetect(false); - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(false); + controller.setUsePhishDetect(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); }); }); describe('setUseMultiAccountBalanceChecker', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(true); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + true, + ); }); it('should set the setUseMultiAccountBalanceChecker property in state', () => { - preferencesController.setUseMultiAccountBalanceChecker(false); - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(false); + controller.setUseMultiAccountBalanceChecker(false); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + false, + ); }); }); describe('isRedesignedConfirmationsFeatureEnabled', () => { + const { controller } = setupController({}); it('isRedesignedConfirmationsFeatureEnabled should default to false', () => { expect( - preferencesController.store.getState().preferences - .isRedesignedConfirmationsDeveloperEnabled, + controller.state.preferences.isRedesignedConfirmationsDeveloperEnabled, ).toStrictEqual(false); }); }); describe('setUseSafeChainsListValidation', function () { + const { controller } = setupController({}); it('should default to true', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useSafeChainsListValidation).toStrictEqual(true); }); it('should set the `setUseSafeChainsListValidation` property in state', function () { - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(true); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(true); - preferencesController.setUseSafeChainsListValidation(false); + controller.setUseSafeChainsListValidation(false); - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(false); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(false); }); }); describe('setUseTokenDetection', function () { + const { controller } = setupController({}); it('should default to true for new users', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useTokenDetection).toStrictEqual(true); }); it('should set the useTokenDetection property in state', () => { - preferencesController.setUseTokenDetection(true); - expect( - preferencesController.store.getState().useTokenDetection, - ).toStrictEqual(true); + controller.setUseTokenDetection(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); }); it('should keep initial value of useTokenDetection for existing users', function () { - // TODO: Remove unregisterActionHandler and clearEventSubscriptions once the PreferencesController has been refactored to use the withController pattern. - controllerMessenger.unregisterActionHandler( - 'PreferencesController:getState', - ); - controllerMessenger.clearEventSubscriptions( - 'PreferencesController:stateChange', - ); - const preferencesControllerExistingUser = new PreferencesController({ - messenger: controllerMessenger.getRestricted({ - name: 'PreferencesController', - allowedActions: [], - allowedEvents: ['AccountsController:stateChange'], - }), - initLangCode: 'en_US', - initState: { - useTokenDetection: false, + const { controller: preferencesControllerExistingUser } = setupController( + { + state: { + useTokenDetection: false, + }, }, - networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - }); - const state = preferencesControllerExistingUser.store.getState(); + ); + const { state } = preferencesControllerExistingUser; expect(state.useTokenDetection).toStrictEqual(false); }); }); describe('setUseNftDetection', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); it('should set the useNftDetection property in state', () => { - preferencesController.setOpenSeaEnabled(true); - preferencesController.setUseNftDetection(true); - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + controller.setUseNftDetection(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); }); describe('setUse4ByteResolution', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(true); + expect(controller.state.use4ByteResolution).toStrictEqual(true); }); it('should set the use4ByteResolution property in state', () => { - preferencesController.setUse4ByteResolution(false); - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(false); + controller.setUse4ByteResolution(false); + expect(controller.state.use4ByteResolution).toStrictEqual(false); }); }); describe('setOpenSeaEnabled', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); it('should set the openSeaEnabled property in state', () => { - preferencesController.setOpenSeaEnabled(true); - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); }); describe('setAdvancedGasFee', () => { + const { controller } = setupController({}); it('should default to an empty object', () => { - expect( - preferencesController.store.getState().advancedGasFee, - ).toStrictEqual({}); + expect(controller.state.advancedGasFee).toStrictEqual({}); }); it('should set the setAdvancedGasFee property in state', () => { - preferencesController.setAdvancedGasFee({ + controller.setAdvancedGasFee({ chainId: CHAIN_IDS.GOERLI, gasFeePreferences: { maxBaseFee: '1.5', @@ -474,51 +438,44 @@ describe('preferences controller', () => { }, }); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .maxBaseFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].maxBaseFee, ).toStrictEqual('1.5'); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .priorityFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].priorityFee, ).toStrictEqual('2'); }); }); describe('setTheme', () => { + const { controller } = setupController({}); it('should default to value "OS"', () => { - expect(preferencesController.store.getState().theme).toStrictEqual('os'); + expect(controller.state.theme).toStrictEqual('os'); }); it('should set the setTheme property in state', () => { - preferencesController.setTheme(ThemeType.dark); - expect(preferencesController.store.getState().theme).toStrictEqual( - 'dark', - ); + controller.setTheme(ThemeType.dark); + expect(controller.state.theme).toStrictEqual('dark'); }); }); describe('setUseCurrencyRateCheck', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); }); it('should set the useCurrencyRateCheck property in state', () => { - preferencesController.setUseCurrencyRateCheck(false); - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(false); + controller.setUseCurrencyRateCheck(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); }); }); describe('setIncomingTransactionsPreferences', () => { + const { controller } = setupController({}); const addedNonTestNetworks = Object.keys(NETWORK_CONFIGURATION_DATA); it('should have default value combined', () => { - const state: { - incomingTransactionsPreferences: Record; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, @@ -533,13 +490,11 @@ describe('preferences controller', () => { }); it('should update incomingTransactionsPreferences with given value set', () => { - preferencesController.setIncomingTransactionsPreferences( + controller.setIncomingTransactionsPreferences( CHAIN_IDS.LINEA_MAINNET, false, ); - const state: { - incomingTransactionsPreferences: Record; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: false, @@ -555,10 +510,11 @@ describe('preferences controller', () => { }); describe('AccountsController:stateChange subscription', () => { + const { controller, messenger, accountsController } = setupController({}); it('sync the identities with the accounts in the accounts controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -574,7 +530,7 @@ describe('preferences controller', () => { const accounts = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; expect(accounts.map((account) => account.address)).toStrictEqual( Object.keys(identities), @@ -584,68 +540,313 @@ describe('preferences controller', () => { ///: BEGIN:ONLY_INCLUDE_IF(petnames) describe('setUseExternalNameSources', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the useExternalNameSources property in state', () => { - preferencesController.setUseExternalNameSources(false); - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(false); + controller.setUseExternalNameSources(false); + expect(controller.state.useExternalNameSources).toStrictEqual(false); }); }); ///: END:ONLY_INCLUDE_IF describe('setUseTransactionSimulations', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the setUseTransactionSimulations property in state', () => { - preferencesController.setUseTransactionSimulations(false); - expect( - preferencesController.store.getState().useTransactionSimulations, - ).toStrictEqual(false); + controller.setUseTransactionSimulations(false); + expect(controller.state.useTransactionSimulations).toStrictEqual(false); }); }); describe('setServiceWorkerKeepAlivePreference', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(true); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(true); }); it('should set the setServiceWorkerKeepAlivePreference property in state', () => { - preferencesController.setServiceWorkerKeepAlivePreference(false); - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(false); + controller.setServiceWorkerKeepAlivePreference(false); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(false); }); }); describe('setBitcoinSupportEnabled', () => { + const { controller } = setupController({}); it('has the default value as false', () => { - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); }); it('sets the bitcoinSupportEnabled property in state to true and then false', () => { - preferencesController.setBitcoinSupportEnabled(true); + controller.setBitcoinSupportEnabled(true); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(true); + + controller.setBitcoinSupportEnabled(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); + }); + }); + + describe('useNonceField', () => { + it('defaults useNonceField to false', () => { + const { controller } = setupController({}); + expect(controller.state.useNonceField).toStrictEqual(false); + }); + + it('setUseNonceField to true', () => { + const { controller } = setupController({}); + controller.setUseNonceField(true); + expect(controller.state.useNonceField).toStrictEqual(true); + }); + }); + + describe('globalThis.setPreference', () => { + it('setFeatureFlags to true', () => { + const { controller } = setupController({}); + globalThis.setPreference('showFiatInTestnets', true); + expect(controller.state.featureFlags.showFiatInTestnets).toStrictEqual( + true, + ); + }); + }); + + describe('useExternalServices', () => { + it('defaults useExternalServices to true', () => { + const { controller } = setupController({}); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); + }); + + it('useExternalServices to false', () => { + const { controller } = setupController({}); + controller.toggleExternalServices(false); + expect(controller.state.useExternalServices).toStrictEqual(false); + expect(controller.state.useTokenDetection).toStrictEqual(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + expect(controller.state.openSeaEnabled).toStrictEqual(false); + expect(controller.state.useNftDetection).toStrictEqual(false); + }); + }); + + describe('useRequestQueue', () => { + it('defaults useRequestQueue to true', () => { + const { controller } = setupController({}); + expect(controller.state.useRequestQueue).toStrictEqual(true); + }); + + it('setUseRequestQueue to false', () => { + const { controller } = setupController({}); + controller.setUseRequestQueue(false); + expect(controller.state.useRequestQueue).toStrictEqual(false); + }); + }); + + describe('addSnapAccountEnabled', () => { + it('defaults addSnapAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(false); + }); + + it('setAddSnapAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setAddSnapAccountEnabled(true); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(true); + }); + }); + + describe('watchEthereumAccountEnabled', () => { + it('defaults watchEthereumAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(false); + }); + + it('setWatchEthereumAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setWatchEthereumAccountEnabled(true); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(true); + }); + }); + + describe('bitcoinTestnetSupportEnabled', () => { + it('defaults bitcoinTestnetSupportEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual( + false, + ); + }); + + it('setBitcoinTestnetSupportEnabled to true', () => { + const { controller } = setupController({}); + controller.setBitcoinTestnetSupportEnabled(true); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual(true); + }); + }); + + describe('knownMethodData', () => { + it('defaults knownMethodData', () => { + const { controller } = setupController({}); + expect(controller.state.knownMethodData).toStrictEqual({}); + }); + + it('addKnownMethodData', () => { + const { controller } = setupController({}); + controller.addKnownMethodData('0x60806040', 'testMethodName'); + expect(controller.state.knownMethodData).toStrictEqual({ + '0x60806040': 'testMethodName', + }); + }); + }); + + describe('featureFlags', () => { + it('defaults featureFlags', () => { + const { controller } = setupController({}); + expect(controller.state.featureFlags).toStrictEqual({}); + }); + + it('setFeatureFlags', () => { + const { controller } = setupController({}); + controller.setFeatureFlag('showConfirmationAdvancedDetails', true); expect( - preferencesController.store.getState().bitcoinSupportEnabled, + controller.state.featureFlags.showConfirmationAdvancedDetails, ).toStrictEqual(true); + }); + }); - preferencesController.setBitcoinSupportEnabled(false); - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + describe('preferences', () => { + it('defaults preferences', () => { + const { controller } = setupController({}); + expect(controller.state.preferences).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + + it('setPreference', () => { + const { controller } = setupController({}); + controller.setPreference('showConfirmationAdvancedDetails', true); + expect(controller.getPreferences()).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: true, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + }); + + describe('ipfsGateway', () => { + it('defaults ipfsGate to dweb.link', () => { + const { controller } = setupController({}); + expect(controller.state.ipfsGateway).toStrictEqual('dweb.link'); + }); + + it('setIpfsGateway to test.link', () => { + const { controller } = setupController({}); + controller.setIpfsGateway('test.link'); + expect(controller.getIpfsGateway()).toStrictEqual('test.link'); + }); + }); + + describe('isIpfsGatewayEnabled', () => { + it('defaults isIpfsGatewayEnabled to true', () => { + const { controller } = setupController({}); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(true); + }); + + it('set isIpfsGatewayEnabled to false', () => { + const { controller } = setupController({}); + controller.setIsIpfsGatewayEnabled(false); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(false); + }); + }); + + describe('useAddressBarEnsResolution', () => { + it('defaults useAddressBarEnsResolution to true', () => { + const { controller } = setupController({}); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + }); + + it('set useAddressBarEnsResolution to false', () => { + const { controller } = setupController({}); + controller.setUseAddressBarEnsResolution(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + }); + }); + + describe('dismissSeedBackUpReminder', () => { + it('defaults dismissSeedBackUpReminder to false', () => { + const { controller } = setupController({}); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(false); + }); + + it('set dismissSeedBackUpReminder to true', () => { + const { controller } = setupController({}); + controller.setDismissSeedBackUpReminder(true); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(true); + }); + }); + + describe('snapsAddSnapAccountModalDismissed', () => { + it('defaults snapsAddSnapAccountModalDismissed to false', () => { + const { controller } = setupController({}); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + false, + ); + }); + + it('set snapsAddSnapAccountModalDismissed to true', () => { + const { controller } = setupController({}); + controller.setSnapsAddSnapAccountModalDismissed(true); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + true, + ); }); }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index cd4cd5e1a5fa..536ec33b34eb 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -1,4 +1,3 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerChangeEvent, AccountsControllerGetAccountByAddressAction, @@ -7,8 +6,18 @@ import { AccountsControllerSetSelectedAccountAction, AccountsControllerState, } from '@metamask/accounts-controller'; -import { Hex } from '@metamask/utils'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { Hex, Json } from '@metamask/utils'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { NetworkControllerGetStateAction } from '@metamask/network-controller'; +import { + ETHERSCAN_SUPPORTED_CHAIN_IDS, + type PreferencesState, +} from '@metamask/preferences-controller'; import { CHAIN_IDS, IPFS_DEFAULT_GATEWAY_URL, @@ -19,7 +28,7 @@ import { ThemeType } from '../../../shared/constants/preferences'; type AccountIdentityEntry = { address: string; name: string; - lastSelected: number | undefined; + lastSelected?: number; }; const mainNetworks = { @@ -38,10 +47,10 @@ const controllerName = 'PreferencesController'; /** * Returns the state of the {@link PreferencesController}. */ -export type PreferencesControllerGetStateAction = { - type: 'PreferencesController:getState'; - handler: () => PreferencesControllerState; -}; +export type PreferencesControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + PreferencesControllerState +>; /** * Actions exposed by the {@link PreferencesController}. @@ -51,10 +60,10 @@ export type PreferencesControllerActions = PreferencesControllerGetStateAction; /** * Event emitted when the state of the {@link PreferencesController} changes. */ -export type PreferencesControllerStateChangeEvent = { - type: 'PreferencesController:stateChange'; - payload: [PreferencesControllerState, []]; -}; +export type PreferencesControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + PreferencesControllerState +>; /** * Events emitted by {@link PreferencesController}. @@ -68,7 +77,8 @@ export type AllowedActions = | AccountsControllerGetAccountByAddressAction | AccountsControllerSetAccountNameAction | AccountsControllerGetSelectedAccountAction - | AccountsControllerSetSelectedAccountAction; + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction; /** * Events that this controller is allowed to subscribe. @@ -84,9 +94,7 @@ export type PreferencesControllerMessenger = RestrictedControllerMessenger< >; type PreferencesControllerOptions = { - networkConfigurationsByChainId?: Record; - initState?: Partial; - initLangCode?: string; + state?: Partial; messenger: PreferencesControllerMessenger; }; @@ -96,6 +104,7 @@ export type Preferences = { showFiatInTestnets: boolean; showTestNetworks: boolean; smartTransactionsOptInStatus: boolean | null; + showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; petnamesEnabled: boolean; @@ -105,169 +114,364 @@ export type Preferences = { showMultiRpcModal: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; + tokenSortConfig: { + key: string; + order: string; + sortCallback: string; + }; + shouldShowAggregatedBalancePopover: boolean; }; -export type PreferencesControllerState = { - selectedAddress: string; +// Omitting showTestNetworks and smartTransactionsOptInStatus, as they already exists here in Preferences type +export type PreferencesControllerState = Omit< + PreferencesState, + 'showTestNetworks' | 'smartTransactionsOptInStatus' +> & { useBlockie: boolean; useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; - useTokenDetection: boolean; - useNftDetection: boolean; use4ByteResolution: boolean; useCurrencyRateCheck: boolean; useRequestQueue: boolean; - openSeaEnabled: boolean; - securityAlertsEnabled: boolean; + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; - addSnapAccountEnabled: boolean; + addSnapAccountEnabled?: boolean; advancedGasFee: Record>; - featureFlags: Record; incomingTransactionsPreferences: Record; knownMethodData: Record; currentLocale: string; - identities: Record; - lostIdentities: Record; forgottenPassword: boolean; preferences: Preferences; - ipfsGateway: string; - isIpfsGatewayEnabled: boolean; useAddressBarEnsResolution: boolean; ledgerTransportType: LedgerTransportTypes; - snapRegistryList: Record; + // TODO: Replace `Json` with correct type + snapRegistryList: Record; theme: ThemeType; - snapsAddSnapAccountModalDismissed: boolean; + snapsAddSnapAccountModalDismissed?: boolean; useExternalNameSources: boolean; - useTransactionSimulations: boolean; enableMV3TimestampSave: boolean; useExternalServices: boolean; textDirection?: string; }; -export default class PreferencesController { - store: ObservableStore; +/** + * Function to get default state of the {@link PreferencesController}. + */ +export const getDefaultPreferencesControllerState = + (): PreferencesControllerState => ({ + selectedAddress: '', + useBlockie: false, + useNonceField: false, + usePhishDetect: true, + dismissSeedBackUpReminder: false, + useMultiAccountBalanceChecker: true, + useSafeChainsListValidation: true, + // set to true means the dynamic list from the API is being used + // set to false will be using the static list from contract-metadata + useTokenDetection: true, + useNftDetection: true, + use4ByteResolution: true, + useCurrencyRateCheck: true, + useRequestQueue: true, + openSeaEnabled: true, + securityAlertsEnabled: true, + watchEthereumAccountEnabled: false, + bitcoinSupportEnabled: false, + bitcoinTestnetSupportEnabled: false, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + addSnapAccountEnabled: false, + ///: END:ONLY_INCLUDE_IF + advancedGasFee: {}, + featureFlags: {}, + incomingTransactionsPreferences: { + ...mainNetworks, + ...testNetworks, + }, + knownMethodData: {}, + currentLocale: '', + identities: {}, + lostIdentities: {}, + forgottenPassword: false, + preferences: { + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + shouldShowAggregatedBalancePopover: true, // by default user should see popover; + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + // ENS decentralized website resolution + ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, + isIpfsGatewayEnabled: true, + useAddressBarEnsResolution: true, + // Ledger transport type is deprecated. We currently only support webhid + // on chrome, and u2f on firefox. + ledgerTransportType: window.navigator.hid + ? LedgerTransportTypes.webhid + : LedgerTransportTypes.u2f, + snapRegistryList: {}, + theme: ThemeType.os, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + snapsAddSnapAccountModalDismissed: false, + ///: END:ONLY_INCLUDE_IF + useExternalNameSources: true, + useTransactionSimulations: true, + enableMV3TimestampSave: true, + // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. + // Whenever useExternalServices is false, certain features will be disabled. + // The flag is true by Default, meaning the toggle is ON by default. + useExternalServices: true, + // from core PreferencesController + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + }); - private messagingSystem: PreferencesControllerMessenger; +/** + * {@link PreferencesController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + selectedAddress: { + persist: true, + anonymous: false, + }, + useBlockie: { + persist: true, + anonymous: true, + }, + useNonceField: { + persist: true, + anonymous: true, + }, + usePhishDetect: { + persist: true, + anonymous: true, + }, + dismissSeedBackUpReminder: { + persist: true, + anonymous: true, + }, + useMultiAccountBalanceChecker: { + persist: true, + anonymous: true, + }, + useSafeChainsListValidation: { + persist: true, + anonymous: false, + }, + useTokenDetection: { + persist: true, + anonymous: true, + }, + useNftDetection: { + persist: true, + anonymous: true, + }, + use4ByteResolution: { + persist: true, + anonymous: true, + }, + useCurrencyRateCheck: { + persist: true, + anonymous: true, + }, + useRequestQueue: { + persist: true, + anonymous: true, + }, + openSeaEnabled: { + persist: true, + anonymous: true, + }, + securityAlertsEnabled: { + persist: true, + anonymous: false, + }, + watchEthereumAccountEnabled: { + persist: true, + anonymous: false, + }, + bitcoinSupportEnabled: { + persist: true, + anonymous: false, + }, + bitcoinTestnetSupportEnabled: { + persist: true, + anonymous: false, + }, + addSnapAccountEnabled: { + persist: true, + anonymous: false, + }, + advancedGasFee: { + persist: true, + anonymous: true, + }, + featureFlags: { + persist: true, + anonymous: true, + }, + incomingTransactionsPreferences: { + persist: true, + anonymous: true, + }, + knownMethodData: { + persist: true, + anonymous: false, + }, + currentLocale: { + persist: true, + anonymous: true, + }, + identities: { + persist: true, + anonymous: false, + }, + lostIdentities: { + persist: true, + anonymous: false, + }, + forgottenPassword: { + persist: true, + anonymous: true, + }, + preferences: { + persist: true, + anonymous: true, + }, + ipfsGateway: { + persist: true, + anonymous: false, + }, + isIpfsGatewayEnabled: { + persist: true, + anonymous: false, + }, + useAddressBarEnsResolution: { + persist: true, + anonymous: true, + }, + ledgerTransportType: { + persist: true, + anonymous: true, + }, + snapRegistryList: { + persist: true, + anonymous: false, + }, + theme: { + persist: true, + anonymous: true, + }, + snapsAddSnapAccountModalDismissed: { + persist: true, + anonymous: false, + }, + useExternalNameSources: { + persist: true, + anonymous: false, + }, + useTransactionSimulations: { + persist: true, + anonymous: true, + }, + enableMV3TimestampSave: { + persist: true, + anonymous: true, + }, + useExternalServices: { + persist: true, + anonymous: false, + }, + textDirection: { + persist: true, + anonymous: false, + }, + isMultiAccountBalancesEnabled: { persist: true, anonymous: true }, + showIncomingTransactions: { persist: true, anonymous: true }, +}; +export class PreferencesController extends BaseController< + typeof controllerName, + PreferencesControllerState, + PreferencesControllerMessenger +> { /** + * Constructs a Preferences controller. * - * @param opts - Overrides the defaults for the initial state of this.store - * @property messenger - The controller messenger - * @property initState The stored object containing a users preferences, stored in local storage - * @property initState.useBlockie The users preference for blockie identicons within the UI - * @property initState.useNonceField The users preference for nonce field within the UI - * @property initState.featureFlags A key-boolean map, where keys refer to features and booleans to whether the - * user wishes to see that feature. - * - * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior. - * @property initState.knownMethodData Contains all data methods known by the user - * @property initState.currentLocale The preferred language locale key - * @property initState.selectedAddress A hex string that matches the currently selected address in the app + * @param options - the controller options + * @param options.messenger - The controller messenger + * @param options.state - The initial controller state */ - constructor(opts: PreferencesControllerOptions) { + constructor({ messenger, state }: PreferencesControllerOptions) { + const { networkConfigurationsByChainId } = messenger.call( + 'NetworkController:getState', + ); + const addedNonMainNetwork: Record = Object.values( - opts.networkConfigurationsByChainId ?? {}, + networkConfigurationsByChainId ?? {}, ).reduce((acc: Record, element) => { acc[element.chainId] = true; return acc; }, {}); - - const initState: PreferencesControllerState = { - selectedAddress: '', - useBlockie: false, - useNonceField: false, - usePhishDetect: true, - dismissSeedBackUpReminder: false, - useMultiAccountBalanceChecker: true, - useSafeChainsListValidation: true, - // set to true means the dynamic list from the API is being used - // set to false will be using the static list from contract-metadata - useTokenDetection: opts?.initState?.useTokenDetection ?? true, - useNftDetection: opts?.initState?.useTokenDetection ?? true, - use4ByteResolution: true, - useCurrencyRateCheck: true, - useRequestQueue: true, - openSeaEnabled: true, - securityAlertsEnabled: true, - watchEthereumAccountEnabled: false, - bitcoinSupportEnabled: false, - bitcoinTestnetSupportEnabled: false, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - addSnapAccountEnabled: false, - ///: END:ONLY_INCLUDE_IF - advancedGasFee: {}, - - // WARNING: Do not use feature flags for security-sensitive things. - // Feature flag toggling is available in the global namespace - // for convenient testing of pre-release features, and should never - // perform sensitive operations. - featureFlags: {}, - incomingTransactionsPreferences: { - ...mainNetworks, - ...addedNonMainNetwork, - ...testNetworks, - }, - knownMethodData: {}, - currentLocale: opts.initLangCode ?? '', - identities: {}, - lostIdentities: {}, - forgottenPassword: false, - preferences: { - autoLockTimeLimit: undefined, - showExtensionInFullSizeView: false, - showFiatInTestnets: false, - showTestNetworks: false, - smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible - useNativeCurrencyAsPrimaryCurrency: true, - hideZeroBalanceTokens: false, - petnamesEnabled: true, - redesignedConfirmationsEnabled: true, - redesignedTransactionsEnabled: true, - featureNotificationsEnabled: false, - showMultiRpcModal: false, - isRedesignedConfirmationsDeveloperEnabled: false, - showConfirmationAdvancedDetails: false, + super({ + messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultPreferencesControllerState(), + incomingTransactionsPreferences: { + ...mainNetworks, + ...addedNonMainNetwork, + ...testNetworks, + }, + ...state, }, - // ENS decentralized website resolution - ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - isIpfsGatewayEnabled: true, - useAddressBarEnsResolution: true, - // Ledger transport type is deprecated. We currently only support webhid - // on chrome, and u2f on firefox. - ledgerTransportType: window.navigator.hid - ? LedgerTransportTypes.webhid - : LedgerTransportTypes.u2f, - snapRegistryList: {}, - theme: ThemeType.os, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - snapsAddSnapAccountModalDismissed: false, - ///: END:ONLY_INCLUDE_IF - useExternalNameSources: true, - useTransactionSimulations: true, - enableMV3TimestampSave: true, - // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. - // Whenever useExternalServices is false, certain features will be disabled. - // The flag is true by Default, meaning the toggle is ON by default. - useExternalServices: true, - ...opts.initState, - }; - - this.store = new ObservableStore(initState); - this.store.setMaxListeners(13); - - this.messagingSystem = opts.messenger; - this.messagingSystem.registerActionHandler( - `PreferencesController:getState`, - () => this.store.getState(), - ); - this.messagingSystem.registerInitialEventPayload({ - eventType: `PreferencesController:stateChange`, - getPayload: () => [this.store.getState(), []], }); this.messagingSystem.subscribe( @@ -286,7 +490,9 @@ export default class PreferencesController { * @param forgottenPassword - whether or not the user has forgotten their password */ setPasswordForgotten(forgottenPassword: boolean): void { - this.store.updateState({ forgottenPassword }); + this.update((state) => { + state.forgottenPassword = forgottenPassword; + }); } /** @@ -295,7 +501,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers blockie indicators */ setUseBlockie(val: boolean): void { - this.store.updateState({ useBlockie: val }); + this.update((state) => { + state.useBlockie = val; + }); } /** @@ -304,7 +512,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to set nonce */ setUseNonceField(val: boolean): void { - this.store.updateState({ useNonceField: val }); + this.update((state) => { + state.useNonceField = val; + }); } /** @@ -313,7 +523,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers phishing domain protection */ setUsePhishDetect(val: boolean): void { - this.store.updateState({ usePhishDetect: val }); + this.update((state) => { + state.usePhishDetect = val; + }); } /** @@ -322,7 +534,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on all security settings */ setUseMultiAccountBalanceChecker(val: boolean): void { - this.store.updateState({ useMultiAccountBalanceChecker: val }); + this.update((state) => { + state.useMultiAccountBalanceChecker = val; + }); } /** @@ -331,11 +545,15 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on validation for manually adding networks */ setUseSafeChainsListValidation(val: boolean): void { - this.store.updateState({ useSafeChainsListValidation: val }); + this.update((state) => { + state.useSafeChainsListValidation = val; + }); } toggleExternalServices(useExternalServices: boolean): void { - this.store.updateState({ useExternalServices }); + this.update((state) => { + state.useExternalServices = useExternalServices; + }); this.setUseTokenDetection(useExternalServices); this.setUseCurrencyRateCheck(useExternalServices); this.setUsePhishDetect(useExternalServices); @@ -350,7 +568,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use the static token list or dynamic token list from the API */ setUseTokenDetection(val: boolean): void { - this.store.updateState({ useTokenDetection: val }); + this.update((state) => { + state.useTokenDetection = val; + }); } /** @@ -359,7 +579,9 @@ export default class PreferencesController { * @param useNftDetection - Whether or not the user prefers to autodetect NFTs. */ setUseNftDetection(useNftDetection: boolean): void { - this.store.updateState({ useNftDetection }); + this.update((state) => { + state.useNftDetection = useNftDetection; + }); } /** @@ -368,7 +590,9 @@ export default class PreferencesController { * @param use4ByteResolution - (Privacy) Whether or not the user prefers to have smart contract name details resolved with 4byte.directory */ setUse4ByteResolution(use4ByteResolution: boolean): void { - this.store.updateState({ use4ByteResolution }); + this.update((state) => { + state.use4ByteResolution = use4ByteResolution; + }); } /** @@ -377,7 +601,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use currency rate check for ETH and tokens. */ setUseCurrencyRateCheck(val: boolean): void { - this.store.updateState({ useCurrencyRateCheck: val }); + this.update((state) => { + state.useCurrencyRateCheck = val; + }); } /** @@ -386,7 +612,9 @@ export default class PreferencesController { * @param val - Whether or not the user wants to have requests queued if network change is required. */ setUseRequestQueue(val: boolean): void { - this.store.updateState({ useRequestQueue: val }); + this.update((state) => { + state.useRequestQueue = val; + }); } /** @@ -395,8 +623,8 @@ export default class PreferencesController { * @param openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data. */ setOpenSeaEnabled(openSeaEnabled: boolean): void { - this.store.updateState({ - openSeaEnabled, + this.update((state) => { + state.openSeaEnabled = openSeaEnabled; }); } @@ -406,8 +634,8 @@ export default class PreferencesController { * @param securityAlertsEnabled - Whether or not the user prefers to use the security alerts. */ setSecurityAlertsEnabled(securityAlertsEnabled: boolean): void { - this.store.updateState({ - securityAlertsEnabled, + this.update((state) => { + state.securityAlertsEnabled = securityAlertsEnabled; }); } @@ -419,8 +647,8 @@ export default class PreferencesController { * enable the "Add Snap accounts" button. */ setAddSnapAccountEnabled(addSnapAccountEnabled: boolean): void { - this.store.updateState({ - addSnapAccountEnabled, + this.update((state) => { + state.addSnapAccountEnabled = addSnapAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -433,8 +661,8 @@ export default class PreferencesController { * enable the "Watch Ethereum account (Beta)" button. */ setWatchEthereumAccountEnabled(watchEthereumAccountEnabled: boolean): void { - this.store.updateState({ - watchEthereumAccountEnabled, + this.update((state) => { + state.watchEthereumAccountEnabled = watchEthereumAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -446,8 +674,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Beta)" button. */ setBitcoinSupportEnabled(bitcoinSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinSupportEnabled, + this.update((state) => { + state.bitcoinSupportEnabled = bitcoinSupportEnabled; }); } @@ -458,8 +686,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Testnet)" button. */ setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinTestnetSupportEnabled, + this.update((state) => { + state.bitcoinTestnetSupportEnabled = bitcoinTestnetSupportEnabled; }); } @@ -469,8 +697,8 @@ export default class PreferencesController { * @param useExternalNameSources - Whether or not to use external name providers in the name controller. */ setUseExternalNameSources(useExternalNameSources: boolean): void { - this.store.updateState({ - useExternalNameSources, + this.update((state) => { + state.useExternalNameSources = useExternalNameSources; }); } @@ -480,8 +708,8 @@ export default class PreferencesController { * @param useTransactionSimulations - Whether or not to use simulations in the transaction confirmations. */ setUseTransactionSimulations(useTransactionSimulations: boolean): void { - this.store.updateState({ - useTransactionSimulations, + this.update((state) => { + state.useTransactionSimulations = useTransactionSimulations; }); } @@ -499,12 +727,12 @@ export default class PreferencesController { chainId: string; gasFeePreferences: Record; }): void { - const { advancedGasFee } = this.store.getState(); - this.store.updateState({ - advancedGasFee: { + const { advancedGasFee } = this.state; + this.update((state) => { + state.advancedGasFee = { ...advancedGasFee, [chainId]: gasFeePreferences, - }, + }; }); } @@ -514,7 +742,9 @@ export default class PreferencesController { * @param val - 'default' or 'dark' value based on the mode selected by user. */ setTheme(val: ThemeType): void { - this.store.updateState({ theme: val }); + this.update((state) => { + state.theme = val; + }); } /** @@ -524,12 +754,14 @@ export default class PreferencesController { * @param methodData - Corresponding data method */ addKnownMethodData(fourBytePrefix: string, methodData: string): void { - const { knownMethodData } = this.store.getState(); + const { knownMethodData } = this.state; const updatedKnownMethodData = { ...knownMethodData }; updatedKnownMethodData[fourBytePrefix] = methodData; - this.store.updateState({ knownMethodData: updatedKnownMethodData }); + this.update((state) => { + state.knownMethodData = updatedKnownMethodData; + }); } /** @@ -541,9 +773,9 @@ export default class PreferencesController { const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key) ? 'rtl' : 'auto'; - this.store.updateState({ - currentLocale: key, - textDirection, + this.update((state) => { + state.currentLocale = key; + state.textDirection = textDirection; }); return textDirection; } @@ -589,7 +821,7 @@ export default class PreferencesController { * @returns whether this option is on or off. */ getUseRequestQueue(): boolean { - return this.store.getState().useRequestQueue; + return this.state.useRequestQueue; } /** @@ -632,14 +864,15 @@ export default class PreferencesController { * @returns the updated featureFlags object. */ setFeatureFlag(feature: string, activated: boolean): Record { - const currentFeatureFlags = this.store.getState().featureFlags; + const currentFeatureFlags = this.state.featureFlags; const updatedFeatureFlags = { ...currentFeatureFlags, [feature]: activated, }; - this.store.updateState({ featureFlags: updatedFeatureFlags }); - + this.update((state) => { + state.featureFlags = updatedFeatureFlags; + }); return updatedFeatureFlags; } @@ -661,7 +894,9 @@ export default class PreferencesController { [preference]: value, }; - this.store.updateState({ preferences: updatedPreferences }); + this.update((state) => { + state.preferences = updatedPreferences; + }); return updatedPreferences; } @@ -671,7 +906,7 @@ export default class PreferencesController { * @returns A map of user-selected preferences. */ getPreferences(): Preferences { - return this.store.getState().preferences; + return this.state.preferences; } /** @@ -680,7 +915,7 @@ export default class PreferencesController { * @returns The current IPFS gateway domain */ getIpfsGateway(): string { - return this.store.getState().ipfsGateway; + return this.state.ipfsGateway; } /** @@ -690,7 +925,9 @@ export default class PreferencesController { * @returns the update IPFS gateway domain */ setIpfsGateway(domain: string): string { - this.store.updateState({ ipfsGateway: domain }); + this.update((state) => { + state.ipfsGateway = domain; + }); return domain; } @@ -700,7 +937,9 @@ export default class PreferencesController { * @param enabled - Whether or not IPFS is enabled */ setIsIpfsGatewayEnabled(enabled: boolean): void { - this.store.updateState({ isIpfsGatewayEnabled: enabled }); + this.update((state) => { + state.isIpfsGatewayEnabled = enabled; + }); } /** @@ -709,7 +948,9 @@ export default class PreferencesController { * @param useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains */ setUseAddressBarEnsResolution(useAddressBarEnsResolution: boolean): void { - this.store.updateState({ useAddressBarEnsResolution }); + this.update((state) => { + state.useAddressBarEnsResolution = useAddressBarEnsResolution; + }); } /** @@ -723,7 +964,9 @@ export default class PreferencesController { setLedgerTransportPreference( ledgerTransportType: LedgerTransportTypes, ): string { - this.store.updateState({ ledgerTransportType }); + this.update((state) => { + state.ledgerTransportType = ledgerTransportType; + }); return ledgerTransportType; } @@ -733,8 +976,8 @@ export default class PreferencesController { * @param dismissSeedBackUpReminder - User preference for dismissing the back up reminder. */ setDismissSeedBackUpReminder(dismissSeedBackUpReminder: boolean): void { - this.store.updateState({ - dismissSeedBackUpReminder, + this.update((state) => { + state.dismissSeedBackUpReminder = dismissSeedBackUpReminder; }); } @@ -745,18 +988,24 @@ export default class PreferencesController { * @param value - preference of certain network, true to be enabled */ setIncomingTransactionsPreferences(chainId: Hex, value: boolean): void { - const previousValue = this.store.getState().incomingTransactionsPreferences; + const previousValue = this.state.incomingTransactionsPreferences; const updatedValue = { ...previousValue, [chainId]: value }; - this.store.updateState({ incomingTransactionsPreferences: updatedValue }); + this.update((state) => { + state.incomingTransactionsPreferences = updatedValue; + }); } setServiceWorkerKeepAlivePreference(value: boolean): void { - this.store.updateState({ enableMV3TimestampSave: value }); + this.update((state) => { + state.enableMV3TimestampSave = value; + }); } ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) setSnapsAddSnapAccountModalDismissed(value: boolean): void { - this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); + this.update((state) => { + state.snapsAddSnapAccountModalDismissed = value; + }); } ///: END:ONLY_INCLUDE_IF @@ -767,7 +1016,7 @@ export default class PreferencesController { newAccountsControllerState.internalAccounts; const selectedAccount = accounts[selectedAccountId]; - const { identities, lostIdentities } = this.store.getState(); + const { identities, lostIdentities } = this.state; const addresses = Object.values(accounts).map((account) => account.address.toLowerCase(), @@ -796,10 +1045,10 @@ export default class PreferencesController { {}, ); - this.store.updateState({ - identities: updatedIdentities, - lostIdentities: updatedLostIdentities, - selectedAddress: selectedAccount?.address || '', // it will be an empty string during onboarding + this.update((state) => { + state.identities = updatedIdentities; + state.lostIdentities = updatedLostIdentities; + state.selectedAddress = selectedAccount?.address || ''; // it will be an empty string during onboarding }); } } diff --git a/app/scripts/controllers/swaps/swaps.test.ts b/app/scripts/controllers/swaps/swaps.test.ts index b79cc421c85e..4ed1b545f170 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; @@ -1172,7 +1175,6 @@ describe('SwapsController', function () { const swapsStxBatchStatusRefreshTime = 0; const swapsStxGetTransactionsRefreshTime = 0; const swapsStxStatusDeadline = 0; - swapsController.__test__updateState({ swapsState: { ...swapsController.state.swapsState, 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/backup.js b/app/scripts/lib/backup.js index 7c550c1581ab..c9da3628a99c 100644 --- a/app/scripts/lib/backup.js +++ b/app/scripts/lib/backup.js @@ -18,7 +18,7 @@ export default class Backup { } async restoreUserData(jsonString) { - const existingPreferences = this.preferencesController.store.getState(); + const existingPreferences = this.preferencesController.state; const { preferences, addressBook, network, internalAccounts } = JSON.parse(jsonString); if (preferences) { @@ -26,7 +26,7 @@ export default class Backup { preferences.lostIdentities = existingPreferences.lostIdentities; preferences.selectedAddress = existingPreferences.selectedAddress; - this.preferencesController.store.updateState(preferences); + this.preferencesController.update(preferences); } if (addressBook) { @@ -51,7 +51,7 @@ export default class Backup { async backupUserData() { const userData = { - preferences: { ...this.preferencesController.store.getState() }, + preferences: { ...this.preferencesController.state }, internalAccounts: { internalAccounts: this.accountsController.state.internalAccounts, }, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 0d9712ba5be5..7a322148c847 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -7,8 +7,7 @@ import { mockNetworkState } from '../../../test/stub/networks'; import Backup from './backup'; function getMockPreferencesController() { - const mcState = { - getSelectedAddress: jest.fn().mockReturnValue('0x01'), + const state = { selectedAddress: '0x01', identities: { '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': { @@ -24,15 +23,14 @@ function getMockPreferencesController() { name: 'Ledger 1', }, }, - update: (store) => (mcState.store = store), }; + const getSelectedAddress = jest.fn().mockReturnValue('0x01'); - mcState.store = { - getState: jest.fn().mockReturnValue(mcState), - updateState: (store) => (mcState.store = store), + return { + state, + getSelectedAddress, + update: jest.fn(), }; - - return mcState; } function getMockAddressBookController() { @@ -239,30 +237,30 @@ describe('Backup', function () { ).toStrictEqual('network-configuration-id-4'); // make sure identities are not lost after restore expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].lastSelected, ).toStrictEqual(1655380342907); expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].name, ).toStrictEqual('Account 3'); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].lastSelected, ).toStrictEqual(1655379648197); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].name, ).toStrictEqual('Ledger 1'); // make sure selected address is not lost after restore - expect(backup.preferencesController.store.selectedAddress).toStrictEqual( + expect(backup.preferencesController.state.selectedAddress).toStrictEqual( '0x01', ); diff --git a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts index a5e04f6b7834..063271a9984a 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts @@ -1,12 +1,12 @@ -import { jsonrpc2 } from '@metamask/utils'; +import { jsonrpc2, Json } from '@metamask/utils'; import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; -import { Json } from 'json-rpc-engine'; +import type { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware, { EvmMethodsToNonEvmAccountFilterMessenger, } from './createEvmMethodsToNonEvmAccountReqFilterMiddleware'; describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { - const getMockRequest = (method: string, params?: Json) => ({ + const getMockRequest = (method: string, params: Json) => ({ jsonrpc: jsonrpc2, id: 1, method, @@ -20,71 +20,85 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { { accountType: BtcAccountType.P2wpkh, method: 'eth_accounts', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_sendRawTransaction', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_sendTransaction', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData_v1', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData_v3', + params: undefined, calledNext: false, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData_v4', + params: undefined, calledNext: false, }, { accountType: EthAccountType.Eoa, method: 'eth_accounts', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_sendRawTransaction', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_sendTransaction', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData_v1', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData_v3', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData_v4', + params: undefined, calledNext: true, }, @@ -92,21 +106,25 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { { accountType: BtcAccountType.P2wpkh, method: 'eth_blockNumber', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'eth_chainId', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_blockNumber', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'eth_chainId', + params: undefined, calledNext: true, }, @@ -114,91 +132,109 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { { accountType: BtcAccountType.P2wpkh, method: 'wallet_getSnaps', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_invokeSnap', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_requestSnaps', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'snap_getClientStatus', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_addEthereumChain', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_getPermissions', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_requestPermissions', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_revokePermissions', + params: undefined, calledNext: true, }, { accountType: BtcAccountType.P2wpkh, method: 'wallet_switchEthereumChain', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_getSnaps', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_invokeSnap', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_requestSnaps', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'snap_getClientStatus', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_addEthereumChain', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_getPermissions', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_requestPermissions', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_revokePermissions', + params: undefined, calledNext: true, }, { accountType: EthAccountType.Eoa, method: 'wallet_switchEthereumChain', + params: undefined, calledNext: true, }, @@ -250,7 +286,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { }: { accountType: EthAccountType | BtcAccountType; method: string; - params?: Json; + params: Json; calledNext: number; }) => { const filterFn = createEvmMethodsToNonEvmAccountReqFilterMiddleware({ @@ -262,7 +298,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { const mockEnd = jest.fn(); filterFn( - getMockRequest(method, params), + getMockRequest(method, params) as JsonRpcRequest, getMockResponse(), mockNext, mockEnd, diff --git a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts index 3e1eca86997e..cc912b5113a7 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.ts @@ -1,7 +1,8 @@ import { isEvmAccountType } from '@metamask/keyring-api'; import { RestrictedControllerMessenger } from '@metamask/base-controller'; import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; -import { JsonRpcMiddleware } from 'json-rpc-engine'; +import { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { Json, JsonRpcParams } from '@metamask/utils'; import { RestrictedEthMethods } from '../../../shared/constants/permissions'; import { unrestrictedEthSigningMethods } from '../controllers/permissions'; @@ -32,7 +33,7 @@ export default function createEvmMethodsToNonEvmAccountReqFilterMiddleware({ messenger, }: { messenger: EvmMethodsToNonEvmAccountFilterMessenger; -}): JsonRpcMiddleware { +}): JsonRpcMiddleware { return function filterEvmRequestToNonEvmAccountsMiddleware( req, _res, @@ -74,7 +75,13 @@ export default function createEvmMethodsToNonEvmAccountReqFilterMiddleware({ // TODO: Convert this to superstruct schema const isWalletRequestPermission = req.method === 'wallet_requestPermissions'; - if (isWalletRequestPermission && req?.params && Array.isArray(req.params)) { + if ( + isWalletRequestPermission && + req?.params && + Array.isArray(req.params) && + req.params.length > 0 && + req.params[0] + ) { const permissionsMethodRequest = Object.keys(req.params[0]); const isEvmPermissionRequest = METHODS_TO_CHECK.some((method) => diff --git a/app/scripts/lib/createMetaRPCHandler.js b/app/scripts/lib/createMetaRPCHandler.js index 77f86d23fe02..9d72620b4013 100644 --- a/app/scripts/lib/createMetaRPCHandler.js +++ b/app/scripts/lib/createMetaRPCHandler.js @@ -1,4 +1,4 @@ -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { rpcErrors, serializeError } from '@metamask/rpc-errors'; import { isStreamWritable } from './stream-utils'; const createMetaRPCHandler = (api, outStream) => { @@ -9,7 +9,7 @@ const createMetaRPCHandler = (api, outStream) => { if (!api[data.method]) { outStream.write({ jsonrpc: '2.0', - error: ethErrors.rpc.methodNotFound({ + error: rpcErrors.methodNotFound({ message: `${data.method} not found`, }), id: data.id, diff --git a/app/scripts/lib/createMetaRPCHandler.test.js b/app/scripts/lib/createMetaRPCHandler.test.js index 842af632e830..873366d53443 100644 --- a/app/scripts/lib/createMetaRPCHandler.test.js +++ b/app/scripts/lib/createMetaRPCHandler.test.js @@ -71,6 +71,7 @@ describe('createMetaRPCHandler', () => { }); streamTest.on('data', (data) => { expect(data.error.message).toStrictEqual('foo-error'); + expect(data.error.data.cause.message).toStrictEqual('foo-error'); streamTest.end(); }); }); diff --git a/app/scripts/lib/createMetamaskMiddleware.js b/app/scripts/lib/createMetamaskMiddleware.js index d48ae32dc4a3..9ea07b0d28e5 100644 --- a/app/scripts/lib/createMetamaskMiddleware.js +++ b/app/scripts/lib/createMetamaskMiddleware.js @@ -1,4 +1,7 @@ -import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'; +import { + createScaffoldMiddleware, + mergeMiddleware, +} from '@metamask/json-rpc-engine'; import { createWalletMiddleware } from '@metamask/eth-json-rpc-middleware'; import { createPendingNonceMiddleware, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index b50905b8cb65..a1c5a036f13f 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,5 +1,5 @@ import { ApprovalType, detectSIWE } from '@metamask/controller-utils'; -import { errorCodes } from 'eth-rpc-errors'; +import { errorCodes } from '@metamask/rpc-errors'; import { isValidAddress } from 'ethereumjs-util'; import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; import { @@ -18,6 +18,7 @@ import { PRIMARY_TYPES_PERMIT, } from '../../../shared/constants/signatures'; import { SIGNING_METHODS } from '../../../shared/constants/transaction'; +import { getErrorMessage } from '../../../shared/modules/error'; import { generateSignatureUniqueId, getBlockaidMetricsProps, @@ -419,15 +420,20 @@ export default function createRPCMethodTrackingMiddleware({ const location = res.error?.data?.location; let event; + + const errorMessage = getErrorMessage(res.error); + if (res.error?.code === errorCodes.provider.userRejectedRequest) { event = eventType.REJECTED; } else if ( res.error?.code === errorCodes.rpc.internal && - res.error?.message === 'Request rejected by user or snap.' + [errorMessage, res.error.message].includes( + 'Request rejected by user or snap.', + ) ) { // The signature was approved in MetaMask but rejected in the snap event = eventType.REJECTED; - eventProperties.status = res.error.message; + eventProperties.status = errorMessage; } else { event = eventType.APPROVED; } diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index f0b66430ee84..01daaf2974a4 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -1,4 +1,4 @@ -import { errorCodes } from 'eth-rpc-errors'; +import { errorCodes } from '@metamask/rpc-errors'; import { detectSIWE } from '@metamask/controller-utils'; import MetaMetricsController from '../controllers/metametrics'; @@ -58,13 +58,11 @@ const metaMetricsController = new MetaMetricsController({ segment: createSegmentMock(2, 10000), getCurrentChainId: () => '0x1338', onNetworkDidChange: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ - currentLocale: 'en_US', - preferences: {}, - })), + preferencesControllerState: { + currentLocale: 'en_US', + preferences: {}, }, + onPreferencesStateChange: jest.fn(), version: '0.0.1', environment: 'test', initState: { diff --git a/app/scripts/lib/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/metaRPCClientFactory.js b/app/scripts/lib/metaRPCClientFactory.js index 3aae9962dbdb..2451189836e9 100644 --- a/app/scripts/lib/metaRPCClientFactory.js +++ b/app/scripts/lib/metaRPCClientFactory.js @@ -1,4 +1,4 @@ -import { EthereumRpcError } from 'eth-rpc-errors'; +import { JsonRpcError } from '@metamask/rpc-errors'; import SafeEventEmitter from '@metamask/safe-event-emitter'; import createRandomId from '../../../shared/modules/random-id'; import { TEN_SECONDS_IN_MILLISECONDS } from '../../../shared/lib/transactions-controller-utils'; @@ -77,7 +77,7 @@ class MetaRPCClient { } if (error) { - const e = new EthereumRpcError(error.code, error.message, error.data); + const e = new JsonRpcError(error.code, error.message, error.data); // preserve the stack from serializeError e.stack = error.stack; if (cb) { diff --git a/app/scripts/lib/middleware/pending.js b/app/scripts/lib/middleware/pending.js index 9e01d11ffcb2..0c9d3445a01e 100644 --- a/app/scripts/lib/middleware/pending.js +++ b/app/scripts/lib/middleware/pending.js @@ -1,4 +1,4 @@ -import { createAsyncMiddleware } from 'json-rpc-engine'; +import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; import { formatTxMetaForRpcResult } from '../util'; export function createPendingNonceMiddleware({ getPendingNonce }) { diff --git a/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts b/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts index 83272015ae2d..0f94627f2836 100644 --- a/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts +++ b/app/scripts/lib/offscreen-bridge/trezor-offscreen-bridge.ts @@ -30,6 +30,8 @@ import { export class TrezorOffscreenBridge implements TrezorBridge { model: string | undefined; + minorVersion: number | undefined; + init( settings: { manifest: Manifest; @@ -40,7 +42,8 @@ export class TrezorOffscreenBridge implements TrezorBridge { msg.target === OffscreenCommunicationTarget.extension && msg.event === OffscreenCommunicationEvents.trezorDeviceConnect ) { - this.model = msg.payload; + this.model = msg.payload.model; + this.minorVersion = msg.payload.minorVersion; } }); diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index 6b7434ea0f8c..8977c00aa3d7 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 { @@ -19,7 +19,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'; @@ -54,17 +57,17 @@ const createMiddleware = ( const ppomController = {}; const preferenceController = { - store: { - getState: () => ({ - securityAlertsEnabled: securityAlertsEnabled ?? true, - }), + state: { + securityAlertsEnabled: securityAlertsEnabled ?? true, }, }; if (error) { - preferenceController.store.getState = () => { - throw error; - }; + Object.defineProperty(preferenceController, 'state', { + get() { + throw error; + }, + }); } const networkController = { @@ -115,6 +118,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/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 1bad576e3881..7eb8dc0cc5a2 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -11,8 +11,8 @@ import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; -import PreferencesController from '../../controllers/preferences-controller'; -import { AppStateController } from '../../controllers/app-state'; +import { PreferencesController } from '../../controllers/preferences-controller'; +import { AppStateController } from '../../controllers/app-state-controller'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; // eslint-disable-next-line import/no-restricted-paths import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; @@ -76,8 +76,7 @@ export function createPPOMMiddleware< next: () => void, ) => { try { - const securityAlertsEnabled = - preferencesController.store.getState()?.securityAlertsEnabled; + const { securityAlertsEnabled } = preferencesController.state; const { chainId } = getProviderConfig({ diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index 2fd1932a649d..ea62c3b88533 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -6,14 +6,16 @@ import { TransactionParams, normalizeTransactionParams, } from '@metamask/transaction-controller'; -import { SignatureController } from '@metamask/signature-controller'; -import type { PersonalMessage } from '@metamask/message-manager'; +import { + SignatureController, + SignatureRequest, +} from '@metamask/signature-controller'; import { BlockaidReason, BlockaidResultType, SecurityAlertSource, } from '../../../../shared/constants/security-provider'; -import { AppStateController } from '../../controllers/app-state'; +import { AppStateController } from '../../controllers/app-state-controller'; import { generateSecurityAlertId, isChainSupported, @@ -246,7 +248,7 @@ describe('PPOM Utils', () => { ...SECURITY_ALERT_RESPONSE_MOCK, securityAlertId: SECURITY_ALERT_ID_MOCK, }, - } as unknown as PersonalMessage, + } as unknown as SignatureRequest, }); await updateSecurityAlertResponse({ diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 73999061a910..7662c364b651 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -15,7 +15,7 @@ import { SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; -import { AppStateController } from '../../controllers/app-state'; +import { AppStateController } from '../../controllers/app-state-controller'; import { SecurityAlertResponse } from './types'; import { getSecurityAlertsAPISupportedChainIds, diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index e4b436163fc6..bbc06e7033f5 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -1,7 +1,7 @@ import { permissionRpcMethods } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; import { selectHooks } from '@metamask/snaps-rpc-methods'; import { hasProperty } from '@metamask/utils'; -import { ethErrors } from 'eth-rpc-errors'; import { handlers as localHandlers, legacyHandlers } from './handlers'; const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers]; @@ -42,7 +42,7 @@ function makeMethodMiddlewareMaker(handlers) { * * @param {Record unknown | Promise>} hooks - Required "hooks" into our * controllers. - * @returns {import('json-rpc-engine').JsonRpcMiddleware} The method middleware function. + * @returns {import('@metamask/json-rpc-engine').JsonRpcMiddleware} The method middleware function. */ const makeMethodMiddleware = (hooks) => { assertExpectedHook(hooks, expectedHookNames); @@ -67,7 +67,7 @@ function makeMethodMiddlewareMaker(handlers) { return end( error instanceof Error ? error - : ethErrors.rpc.internal({ data: error }), + : rpcErrors.internal({ data: error }), ); } } diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js index 46aba9abe746..48ea5ae90d58 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js @@ -1,4 +1,4 @@ -import { JsonRpcEngine } from 'json-rpc-engine'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { assertIsJsonRpcFailure, assertIsJsonRpcSuccess, @@ -140,6 +140,7 @@ describe.each([ assertIsJsonRpcFailure(response); expect(response.error.message).toBe('test error'); + expect(response.error.data.cause.message).toBe('test error'); }); it('should handle errors thrown by the implementation', async () => { @@ -156,6 +157,7 @@ describe.each([ assertIsJsonRpcFailure(response); expect(response.error.message).toBe('test error'); + expect(response.error.data.cause.message).toBe('test error'); }); it('should handle non-errors thrown by the implementation', async () => { diff --git a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts index 193cc54b5a38..c96201041d36 100644 --- a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts +++ b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts @@ -1,5 +1,6 @@ -import { ethErrors } from 'eth-rpc-errors'; -import type { JsonRpcMiddleware } from 'json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcParams } from '@metamask/utils'; +import { rpcErrors } from '@metamask/rpc-errors'; import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; /** @@ -7,12 +8,12 @@ import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; * appropriate error. */ export function createUnsupportedMethodMiddleware(): JsonRpcMiddleware< - unknown, - void + JsonRpcParams, + null > { return async function unsupportedMethodMiddleware(req, _res, next, end) { if ((UNSUPPORTED_RPC_METHODS as Set).has(req.method)) { - return end(ethErrors.rpc.methodNotSupported()); + return end(rpcErrors.methodNotSupported()); } return next(); }; 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..afcc2e167043 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 @@ -1,7 +1,7 @@ -import { ApprovalType } from '@metamask/controller-utils'; import * as URI from 'uri-js'; +import { ApprovalType } from '@metamask/controller-utils'; import { RpcEndpointType } from '@metamask/network-controller'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { cloneDeep } from 'lodash'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { @@ -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 ( @@ -76,7 +73,7 @@ async function addEthereumChainHandler( existingNetwork.nativeCurrency !== ticker ) { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\n${ticker}`, }), ); @@ -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..ee0c9d3f732b 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 @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; import addEthereumChain from './add-ethereum-chain'; @@ -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( @@ -365,7 +350,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\ninvalid_chain_id`, }), ); @@ -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'; @@ -592,7 +573,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${unexpectedParam}`, }), ); @@ -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() @@ -676,7 +657,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\nWRONG`, }), ); @@ -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/eth-accounts.ts b/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts index 003cbd88281b..47c2f0c2e318 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.ts @@ -1,7 +1,7 @@ import type { JsonRpcEngineEndCallback, JsonRpcEngineNextCallback, -} from 'json-rpc-engine'; +} from '@metamask/json-rpc-engine'; import type { JsonRpcRequest, JsonRpcParams, 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..10973e052715 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 { errorCodes, rpcErrors } from '@metamask/rpc-errors'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -12,13 +11,13 @@ import { getValidUrl } from '../../util'; export function validateChainId(chainId) { const _chainId = typeof chainId === 'string' && chainId.toLowerCase(); if (!isPrefixedFormattedHexString(_chainId)) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`, }); } if (!isSafeChainId(parseInt(_chainId, 16))) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`, }); } @@ -28,7 +27,7 @@ export function validateChainId(chainId) { export function validateSwitchEthereumChainParams(req, end) { if (!req.params?.[0] || typeof req.params[0] !== 'object') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( req.params, )}`, @@ -37,7 +36,7 @@ export function validateSwitchEthereumChainParams(req, end) { const { chainId, ...otherParams } = req.params[0]; if (Object.keys(otherParams).length > 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${Object.keys( otherParams, )}`, @@ -49,7 +48,7 @@ export function validateSwitchEthereumChainParams(req, end) { export function validateAddEthereumChainParams(params, end) { if (!params || typeof params !== 'object') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( params, )}`, @@ -71,14 +70,14 @@ export function validateAddEthereumChainParams(params, end) { ); if (otherKeys.length > 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${otherKeys}`, }); } const _chainId = validateChainId(chainId, end); if (!rpcUrls || !Array.isArray(rpcUrls) || rpcUrls.length === 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, }); } @@ -101,13 +100,13 @@ export function validateAddEthereumChainParams(params, end) { : null; if (!firstValidRPCUrl) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, }); } if (typeof chainName !== 'string' || !chainName) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected non-empty string 'chainName'. Received:\n${chainName}`, }); } @@ -117,18 +116,18 @@ export function validateAddEthereumChainParams(params, end) { if (nativeCurrency !== null) { if (typeof nativeCurrency !== 'object' || Array.isArray(nativeCurrency)) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected null or object 'nativeCurrency'. Received:\n${nativeCurrency}`, }); } if (nativeCurrency.decimals !== 18) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected the number 18 for 'nativeCurrency.decimals' when 'nativeCurrency' is provided. Received: ${nativeCurrency.decimals}`, }); } if (!nativeCurrency.symbol || typeof nativeCurrency.symbol !== 'string') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected a string 'nativeCurrency.symbol'. Received: ${nativeCurrency.symbol}`, }); } @@ -139,7 +138,7 @@ export function validateAddEthereumChainParams(params, end) { ticker !== UNKNOWN_TICKER_SYMBOL && (typeof ticker !== 'string' || ticker.length < 1 || ticker.length > 6) ) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected 1-6 character string 'nativeCurrency.symbol'. Received:\n${ticker}`, }); } @@ -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..078bd7866a31 --- /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 '@metamask/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..514f8af6dfa7 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts @@ -0,0 +1,80 @@ +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from '@metamask/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/institutional/mmi-authenticate.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js index 48014a6d66fa..e30332651c8c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-authenticate.js @@ -21,8 +21,8 @@ export default mmiAuthenticate; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js index 1e05251a25c4..428997bff70c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-check-if-token-is-present.js @@ -23,8 +23,8 @@ export default mmiAuthenticate; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 options0 diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js index 61af6eb43fe8..7e685e1754a1 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-add-hardware-wallet.js @@ -16,8 +16,8 @@ export default mmiOpenAddHardwareWallet; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js index cbe96127682f..7f76d3239afb 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-portfolio.js @@ -22,8 +22,8 @@ export default mmiPortfolio; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {WatchAssetOptions} options diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js index 9dca692601a3..46754e144fab 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js @@ -1,5 +1,5 @@ -import { ethErrors } from 'eth-rpc-errors'; import { isAllowedRPCOrigin } from '@metamask-institutional/rpc-allowlist'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../../shared/constants/app'; const mmiSetAccountAndNetwork = { @@ -23,8 +23,8 @@ export default mmiSetAccountAndNetwork; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {WatchAssetOptions} options @@ -46,7 +46,7 @@ async function mmiSetAccountAndNetworkHandler( if (!req.params?.[0] || typeof req.params[0] !== 'object') { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( req.params, )}`, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js index 5aa987ed880f..e72a1aebf830 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-supported.js @@ -19,8 +19,8 @@ export default mmiSupported; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} _req - The JSON-RPC request object. + * @param {import('@metamask/utils').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. */ 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..1b48b75b5e4d --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts @@ -0,0 +1,46 @@ +import type { JsonRpcEngineEndCallback } from '@metamask/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..c91bd4fa4650 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts @@ -0,0 +1,74 @@ +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from '@metamask/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/request-accounts.js b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js index f90fb5bd0d42..68b52ea75549 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { MetaMetricsEventName, @@ -48,8 +48,8 @@ const locks = new Set(); /** * - * @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 {import('@metamask/utils').JsonRpcRequest} _req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {RequestEthereumAccountsOptions} options - The RPC method hooks. @@ -71,7 +71,7 @@ async function requestEthereumAccountsHandler( }, ) { if (locks.has(origin)) { - res.error = ethErrors.rpc.resourceUnavailable( + res.error = rpcErrors.resourceUnavailable( `Already processing ${MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS}. Please wait.`, ); return end(); @@ -132,7 +132,7 @@ async function requestEthereumAccountsHandler( } else { // This should never happen, because it should be caught in the // above catch clause - res.error = ethErrors.rpc.internal( + res.error = rpcErrors.internal( 'Accounts unexpectedly unavailable. Please report this bug.', ); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js index a32fa497f248..5dcfdf274fb6 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; /** @@ -25,8 +25,8 @@ export default sendMetadata; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {SendMetadataOptions} options @@ -50,7 +50,7 @@ function sendMetadataHandler( origin, }); } else { - return end(ethErrors.rpc.invalidParams({ data: params })); + return end(rpcErrors.invalidParams({ data: params })); } res.result = true; 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..5f907bef4d4b 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 @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { validateSwitchEthereumChainParams, @@ -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; @@ -59,34 +57,17 @@ async function switchEthereumChainHandler( if (!networkClientIdToSwitchTo) { return end( - ethErrors.provider.custom({ + providerErrors.custom({ code: 4902, message: `Unrecognized chain ID "${chainId}". Try adding the chain using ${MESSAGE_TYPE.ADD_ETHEREUM_CHAIN} first.`, }), ); } - 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/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index 6bce7d7dca18..2f3475f7df71 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -1,5 +1,5 @@ import { ERC1155, ERC721 } from '@metamask/controller-utils'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; const watchAsset = { @@ -23,8 +23,8 @@ export default watchAsset; */ /** - * @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 {import('@metamask/utils').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('@metamask/utils').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 {WatchAssetOptions} options @@ -51,7 +51,7 @@ async function watchAssetHandler( typeof tokenId !== 'string' ) { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected parameter 'tokenId' to be type 'string'. Received type '${typeof tokenId}'`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js index eebe8a470ead..73efdd2a2798 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js @@ -1,5 +1,5 @@ import { ERC20, ERC721 } from '@metamask/controller-utils'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import watchAssetHandler from './watch-asset'; describe('watchAssetHandler', () => { @@ -95,7 +95,7 @@ describe('watchAssetHandler', () => { }); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected parameter 'tokenId' to be type 'string'. Received type 'number'`, }), ); diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 55d04bfb0754..1b9e9f4ddbfc 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.015; + } + return 0; } if (METAMASK_DEBUG) { @@ -227,8 +238,8 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - getManifestFlags().doNotForceSentryForThisTest || - (process.env.IN_TEST && !SENTRY_DSN_DEV) + process.env.IN_TEST && + (!SENTRY_DSN_DEV || !getManifestFlags().sentry?.forceEnable) ) { 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/lib/tx-verification/tx-verification-middleware.ts b/app/scripts/lib/tx-verification/tx-verification-middleware.ts index 5349feaf1cf9..7abdf73e3637 100644 --- a/app/scripts/lib/tx-verification/tx-verification-middleware.ts +++ b/app/scripts/lib/tx-verification/tx-verification-middleware.ts @@ -2,14 +2,17 @@ import { hashMessage } from '@ethersproject/hash'; import { verifyMessage } from '@ethersproject/wallet'; import type { NetworkController } from '@metamask/network-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { Json, JsonRpcParams, Hex } from '@metamask/utils'; -import { hasProperty, isObject } from '@metamask/utils'; import type { + Json, + JsonRpcParams, JsonRpcResponse, + Hex, +} from '@metamask/utils'; +import { hasProperty, isObject, JsonRpcRequest } from '@metamask/utils'; +import type { JsonRpcEngineEndCallback, JsonRpcEngineNextCallback, -} from 'json-rpc-engine'; -import { JsonRpcRequest } from 'json-rpc-engine'; +} from '@metamask/json-rpc-engine'; import { EXPERIENCES_TO_VERIFY, getExperience, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ea8fa297cd2f..c33485f665b7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -13,9 +13,9 @@ import { RatesController, fetchMultiExchangeRate, } from '@metamask/assets-controllers'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; -import { JsonRpcEngine } from 'json-rpc-engine'; import { createEngineStream } from 'json-rpc-middleware-stream'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; import { debounce, throttle, memoize, wrap } from 'lodash'; @@ -27,9 +27,9 @@ import createFilterMiddleware from '@metamask/eth-json-rpc-filters'; import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; import { errorCodes as rpcErrorCodes, - EthereumRpcError, - ethErrors, -} from 'eth-rpc-errors'; + JsonRpcError, + providerErrors, +} from '@metamask/rpc-errors'; import { Mutex } from 'await-semaphore'; import log from 'loglevel'; @@ -42,6 +42,7 @@ import { LedgerIframeBridge, } from '@metamask/eth-ledger-bridge-keyring'; import LatticeKeyring from 'eth-lattice-keyring'; +import { rawChainData } from 'eth-chainlist'; import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; import EthQuery from '@metamask/eth-query'; import EthJSQuery from '@metamask/ethjs-query'; @@ -170,6 +171,7 @@ import { } from '../../shared/constants/swaps'; import { CHAIN_IDS, + CHAIN_SPEC_URL, NETWORK_TYPES, NetworkStatus, MAINNET_DISPLAY_NAME, @@ -201,6 +203,10 @@ import { } from '../../shared/constants/metametrics'; import { LOG_EVENT } from '../../shared/constants/logs'; +import { + getStorageItem, + setStorageItem, +} from '../../shared/lib/storage-helpers'; import { getTokenIdParam, fetchTokenBalance, @@ -216,9 +222,9 @@ import { getIsSmartTransaction, isHardwareWallet, getFeatureFlagsByChainId, - getSmartTransactionsOptInStatus, getCurrentChainSupportsSmartTransactions, getHardwareWalletType, + getSmartTransactionsPreferenceEnabled, } from '../../shared/modules/selectors'; import { createCaipStream } from '../../shared/modules/caip-stream'; import { BaseUrl } from '../../shared/constants/urls'; @@ -233,6 +239,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) @@ -275,7 +283,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 { @@ -289,9 +297,9 @@ import { NetworkOrderController } from './controllers/network-order'; import { AccountOrderController } from './controllers/account-order'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { isStreamWritable, setupMultiplex } from './lib/stream-utils'; -import PreferencesController from './controllers/preferences-controller'; -import AppStateController from './controllers/app-state'; -import AlertController from './controllers/alert'; +import { PreferencesController } from './controllers/preferences-controller'; +import { AppStateController } from './controllers/app-state-controller'; +import { AlertController } from './controllers/alert-controller'; import OnboardingController from './controllers/onboarding'; import Backup from './lib/backup'; import DecryptMessageController from './controllers/decrypt-message'; @@ -313,10 +321,11 @@ import { CaveatFactories, CaveatMutatorFactories, getCaveatSpecifications, - getChangedAccounts, + diffMap, getPermissionBackgroundApiMethods, getPermissionSpecifications, getPermittedAccountsByOrigin, + getPermittedChainsByOrigin, NOTIFICATION_NAMES, PermissionNames, unrestrictedMethods, @@ -333,6 +342,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'; @@ -344,7 +356,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 { @@ -372,6 +387,9 @@ export const METAMASK_CONTROLLER_EVENTS = { // stream channels const PHISHING_SAFELIST = 'metamask-phishing-safelist'; +// OneKey devices can connect to Metamask using Trezor USB transport. They use a specific device minor version (99) to differentiate between genuine Trezor and OneKey devices. +export const ONE_KEY_VIA_TREZOR_MINOR_VERSION = 99; + export default class MetamaskController extends EventEmitter { /** * @param {object} opts @@ -405,6 +423,8 @@ export default class MetamaskController extends EventEmitter { this.getRequestAccountTabIds = opts.getRequestAccountTabIds; this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds; + this.initializeChainlist(); + this.controllerMessenger = new ControllerMessenger(); this.loggingController = new LoggingController({ @@ -463,7 +483,7 @@ export default class MetamaskController extends EventEmitter { this.encryptionPublicKeyController.clearUnapproved(); this.decryptMessageController.clearUnapproved(); this.signatureController.clearUnapproved(); - this.approvalController.clear(ethErrors.provider.userRejectedRequest()); + this.approvalController.clear(providerErrors.userRejectedRequest()); }; this.queuedRequestController = new QueuedRequestController({ @@ -592,19 +612,20 @@ export default class MetamaskController extends EventEmitter { name: 'PreferencesController', allowedActions: [ 'AccountsController:setSelectedAccount', + 'AccountsController:getSelectedAccount', 'AccountsController:getAccountByAddress', 'AccountsController:setAccountName', + 'NetworkController:getState', ], allowedEvents: ['AccountsController:stateChange'], }); this.preferencesController = new PreferencesController({ - initState: initState.PreferencesController, - initLangCode: opts.initLangCode, + state: { + currentLocale: opts.initLangCode ?? '', + ...initState.PreferencesController, + }, messenger: preferencesMessenger, - provider: this.provider, - networkConfigurationsByChainId: - this.networkController.state.networkConfigurationsByChainId, }); const tokenListMessenger = this.controllerMessenger.getRestricted({ @@ -616,7 +637,7 @@ export default class MetamaskController extends EventEmitter { this.tokenListController = new TokenListController({ chainId: getCurrentChainId({ metamask: this.networkController.state }), preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( - this.preferencesController.store.getState(), + this.preferencesController.state, ), messenger: tokenListMessenger, state: initState.TokenListController, @@ -730,16 +751,19 @@ export default class MetamaskController extends EventEmitter { addNft: this.nftController.addNft.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] - disabled: - this.preferencesController.store.getState().useNftDetection === - undefined - ? false // the detection is enabled by default - : !this.preferencesController.store.getState().useNftDetection, + disabled: !this.preferencesController.state.useNftDetection, }); this.metaMetricsController = new MetaMetricsController({ segment, - preferencesStore: this.preferencesController.store, + onPreferencesStateChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', + ), + preferencesControllerState: { + currentLocale: this.preferencesController.state.currentLocale, + selectedAddress: this.preferencesController.state.selectedAddress, + }, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:networkDidChange', @@ -826,14 +850,17 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - preferencesStore: this.preferencesController.store, messenger: this.controllerMessenger.getRestricted({ name: 'AppStateController', allowedActions: [ `${this.approvalController.name}:addRequest`, `${this.approvalController.name}:acceptRequest`, + `PreferencesController:getState`, + ], + allowedEvents: [ + `KeyringController:qrKeyringStateChange`, + 'PreferencesController:stateChange', ], - allowedEvents: [`KeyringController:qrKeyringStateChange`], }), extension: this.extension, }); @@ -852,7 +879,7 @@ export default class MetamaskController extends EventEmitter { this.currencyRateController, ); this.currencyRateController.fetchExchangeRate = (...args) => { - if (this.preferencesController.store.getState().useCurrencyRateCheck) { + if (this.preferencesController.state.useCurrencyRateCheck) { return initialFetchExchangeRate(...args); } return { @@ -890,9 +917,10 @@ export default class MetamaskController extends EventEmitter { state: initState.PPOMController, chainId: getCurrentChainId({ metamask: this.networkController.state }), securityAlertsEnabled: - this.preferencesController.store.getState().securityAlertsEnabled, - onPreferencesChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, + this.preferencesController.state.securityAlertsEnabled, + onPreferencesChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', ), cdnBaseUrl: process.env.BLOCKAID_FILE_CDN, blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY, @@ -978,7 +1006,8 @@ export default class MetamaskController extends EventEmitter { tokenPricesService: new CodefiTokenPricesServiceV2(), }); - this.preferencesController.store.subscribe( + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', previousValueComparator((prevState, currState) => { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; @@ -987,7 +1016,7 @@ export default class MetamaskController extends EventEmitter { } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { this.tokenRatesController.stop(); } - }, this.preferencesController.store.getState()), + }, this.preferencesController.state), ); this.ensController = new EnsController({ @@ -1219,7 +1248,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( @@ -1251,9 +1280,13 @@ export default class MetamaskController extends EventEmitter { }), state: initState.SelectedNetworkController, useRequestQueuePreference: - this.preferencesController.store.getState().useRequestQueue, - onPreferencesStateChange: (listener) => - this.preferencesController.store.subscribe(listener), + this.preferencesController.state.useRequestQueue, + onPreferencesStateChange: (listener) => { + preferencesMessenger.subscribe( + 'PreferencesController:stateChange', + listener, + ); + }, domainProxyMap: new WeakRefObjectMap(), }); @@ -1358,8 +1391,7 @@ export default class MetamaskController extends EventEmitter { getFeatureFlags: () => { return { disableSnaps: - this.preferencesController.store.getState().useExternalServices === - false, + this.preferencesController.state.useExternalServices === false, }; }, }); @@ -1459,6 +1491,7 @@ export default class MetamaskController extends EventEmitter { `${this.phishingController.name}:testOrigin`, `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, + `${this.snapController.name}:get`, ], }); @@ -1652,11 +1685,25 @@ 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', + 'PreferencesController: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,21 +1712,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 @@ -1761,8 +1793,7 @@ export default class MetamaskController extends EventEmitter { }); this.alertController = new AlertController({ - initState: initState.AlertController, - preferencesStore: this.preferencesController.store, + state: initState.AlertController, controllerMessenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], @@ -1845,15 +1876,18 @@ export default class MetamaskController extends EventEmitter { getNetworkState: () => this.networkController.state, getPermittedAccounts: this.getPermittedAccounts.bind(this), getSavedGasFees: () => - this.preferencesController.store.getState().advancedGasFee[ + this.preferencesController.state.advancedGasFee[ getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { + etherscanApiKeysByChainId: { + [CHAIN_IDS.MAINNET]: process.env.ETHERSCAN_API_KEY, + [CHAIN_IDS.SEPOLIA]: process.env.ETHERSCAN_API_KEY, + }, includeTokenTransfers: false, isEnabled: () => Boolean( - this.preferencesController.store.getState() - .incomingTransactionsPreferences?.[ + this.preferencesController.state.incomingTransactionsPreferences?.[ getCurrentChainId({ metamask: this.networkController.state }) ] && this.onboardingController.state.completedOnboarding, ), @@ -1862,7 +1896,7 @@ export default class MetamaskController extends EventEmitter { }, isMultichainEnabled: process.env.TRANSACTION_MULTICHAIN, isSimulationEnabled: () => - this.preferencesController.store.getState().useTransactionSimulations, + this.preferencesController.state.useTransactionSimulations, messenger: transactionControllerMessenger, onNetworkStateChange: (listener) => { networkControllerMessenger.subscribe( @@ -1874,7 +1908,7 @@ export default class MetamaskController extends EventEmitter { isResubmitEnabled: () => { const state = this._getMetaMaskState(); return !( - getSmartTransactionsOptInStatus(state) && + getSmartTransactionsPreferenceEnabled(state) && getCurrentChainSupportsSmartTransactions(state) ); }, @@ -1957,6 +1991,7 @@ export default class MetamaskController extends EventEmitter { getAllState: this.getState.bind(this), getCurrentChainId: () => getCurrentChainId({ metamask: this.networkController.state }), + trace, }); this.signatureController.hub.on( @@ -1997,7 +2032,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, @@ -2126,7 +2161,7 @@ export default class MetamaskController extends EventEmitter { }); const isExternalNameSourcesEnabled = () => - this.preferencesController.store.getState().useExternalNameSources; + this.preferencesController.state.useExternalNameSources; this.nameController = new NameController({ messenger: this.controllerMessenger.getRestricted({ @@ -2206,11 +2241,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(); }, ); @@ -2249,22 +2284,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) @@ -2317,7 +2357,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, @@ -2336,7 +2376,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, TransactionController: this.txController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, @@ -2391,7 +2431,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, NetworkController: this.networkController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, @@ -2442,7 +2482,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, ), @@ -2512,7 +2554,7 @@ export default class MetamaskController extends EventEmitter { } postOnboardingInitialization() { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; this.networkController.lookupNetwork(); @@ -2521,8 +2563,7 @@ export default class MetamaskController extends EventEmitter { } // post onboarding emit detectTokens event - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useTokenDetection, useNftDetection } = preferencesControllerState ?? {}; this.metaMetricsController.trackEvent({ @@ -2542,12 +2583,11 @@ export default class MetamaskController extends EventEmitter { } triggerNetworkrequests() { - this.accountTracker.start(); + this.accountTrackerController.start(); this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2561,12 +2601,11 @@ export default class MetamaskController extends EventEmitter { } stopNetworkRequests() { - this.accountTracker.stop(); + this.accountTrackerController.stop(); this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2690,7 +2729,7 @@ export default class MetamaskController extends EventEmitter { * @returns The currently selected locale. */ getLocale() { - const { currentLocale } = this.preferencesController.store.getState(); + const { currentLocale } = this.preferencesController.state; return currentLocale; } @@ -2751,8 +2790,7 @@ export default class MetamaskController extends EventEmitter { 'SnapController:updateSnapState', ), maybeUpdatePhishingList: () => { - const { usePhishDetect } = - this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -2823,11 +2861,23 @@ export default class MetamaskController extends EventEmitter { */ setupControllerEventSubscriptions() { let lastSelectedAddress; + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', + previousValueComparator(async (prevState, currState) => { + const { currentLocale } = currState; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); - this.preferencesController.store.subscribe( - previousValueComparator((prevState, currState) => { - this.#onPreferencesControllerStateChange(currState, prevState); - }, this.preferencesController.store.getState()), + await updateCurrentLocale(currentLocale); + if (currState.incomingTransactionsPreferences?.[chainId]) { + this.txController.startIncomingTransactionPolling(); + } else { + this.txController.stopIncomingTransactionPolling(); + } + + this.#checkTokenListPolling(currState, prevState); + }, this.preferencesController.state), ); this.controllerMessenger.subscribe( @@ -2845,7 +2895,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); @@ -2854,6 +2904,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 () => { @@ -3218,6 +3302,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, @@ -3312,6 +3403,7 @@ export default class MetamaskController extends EventEmitter { connectHardware: this.connectHardware.bind(this), forgetDevice: this.forgetDevice.bind(this), checkHardwareStatus: this.checkHardwareStatus.bind(this), + getDeviceNameForMetric: this.getDeviceNameForMetric.bind(this), unlockHardwareWalletAccount: this.unlockHardwareWalletAccount.bind(this), attemptLedgerTransportCreation: this.attemptLedgerTransportCreation.bind(this), @@ -3481,6 +3573,8 @@ export default class MetamaskController extends EventEmitter { ), setOnboardingDate: appStateController.setOnboardingDate.bind(appStateController), + setLastViewedUserSurvey: + appStateController.setLastViewedUserSurvey.bind(appStateController), setNewPrivacyPolicyToastClickedOrClosed: appStateController.setNewPrivacyPolicyToastClickedOrClosed.bind( appStateController, @@ -3547,6 +3641,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( @@ -3686,6 +3781,9 @@ export default class MetamaskController extends EventEmitter { appStateController.setCustodianDeepLink.bind(appStateController), setNoteToTraderMessage: appStateController.setNoteToTraderMessage.bind(appStateController), + logAndStoreApiRequest: this.mmiController.logAndStoreApiRequest.bind( + this.mmiController, + ), ///: END:ONLY_INCLUDE_IF // snaps @@ -3835,6 +3933,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( @@ -3966,6 +4073,10 @@ export default class MetamaskController extends EventEmitter { userStorageController.syncInternalAccountsWithUserStorage.bind( userStorageController, ), + deleteAccountSyncingDataFromUserStorage: + userStorageController.performDeleteStorageAllFeatureEntries.bind( + userStorageController, + ), // NotificationServicesController checkAccountsPresence: @@ -4063,7 +4174,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 }) => @@ -4219,8 +4330,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(); @@ -4317,14 +4428,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); @@ -4382,9 +4493,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(); } @@ -4561,7 +4672,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; } @@ -4577,6 +4688,26 @@ export default class MetamaskController extends EventEmitter { return keyring.isUnlocked(); } + /** + * Get hardware device name for metric logging. + * + * @param deviceName - HardwareDeviceNames + * @param hdPath - string + * @returns {Promise} + */ + async getDeviceNameForMetric(deviceName, hdPath) { + if (deviceName === HardwareDeviceNames.trezor) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath); + const { minorVersion } = keyring.bridge; + // Specific case for OneKey devices, see `ONE_KEY_VIA_TREZOR_MINOR_VERSION` for further details. + if (minorVersion && minorVersion === ONE_KEY_VIA_TREZOR_MINOR_VERSION) { + return HardwareDeviceNames.oneKeyViaTrezor; + } + } + + return deviceName; + } + /** * Clear * @@ -4649,9 +4780,11 @@ export default class MetamaskController extends EventEmitter { /** * get hardware account label * + * @param name + * @param index + * @param hdPathDescription * @returns string label */ - getAccountLabel(name, index, hdPathDescription) { return `${name[0].toUpperCase()}${name.slice(1)} ${ parseInt(index, 10) + 1 @@ -4696,7 +4829,7 @@ export default class MetamaskController extends EventEmitter { const accounts = this.accountsController.listAccounts(); - const { identities } = this.preferencesController.store.getState(); + const { identities } = this.preferencesController.state; return { unlockedAccount, identities, accounts }; } @@ -4877,6 +5010,7 @@ export default class MetamaskController extends EventEmitter { transactionParams, transactionOptions, dappRequest, + ...otherParams }) { return { internalAccounts: this.accountsController.listAccounts(), @@ -4894,8 +5028,9 @@ export default class MetamaskController extends EventEmitter { chainId: getCurrentChainId({ metamask: this.networkController.state }), ppomController: this.ppomController, securityAlertsEnabled: - this.preferencesController.store.getState()?.securityAlertsEnabled, + this.preferencesController.state?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), + ...otherParams, }; } @@ -5065,7 +5200,7 @@ export default class MetamaskController extends EventEmitter { }) { if (sender.url) { if (this.onboardingController.state.completedOnboarding) { - if (this.preferencesController.store.getState().usePhishDetect) { + if (this.preferencesController.state.usePhishDetect) { const { hostname } = new URL(sender.url); this.phishingController.maybeUpdateState(); // Check if new connection is blocked if phishing detection is on @@ -5164,7 +5299,7 @@ export default class MetamaskController extends EventEmitter { * @param {ReadableStream} options.connectionStream - The Duplex stream to connect to. */ setupPhishingCommunication({ connectionStream }) { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -5384,11 +5519,7 @@ export default class MetamaskController extends EventEmitter { outStream, (err) => { // handle any middleware cleanup - engine._middleware.forEach((mid) => { - if (mid.destroy && typeof mid.destroy === 'function') { - mid.destroy(); - } - }); + engine.destroy(); connectionId && this.removeConnection(origin, connectionId); // For context and todos related to the error message match, see https://github.com/MetaMask/metamask-extension/issues/26337 if (err && !err.message?.match('Premature close')) { @@ -5558,7 +5689,7 @@ export default class MetamaskController extends EventEmitter { ); const isConfirmationRedesignEnabled = () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .redesignedConfirmationsEnabled; }; @@ -5667,7 +5798,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 +5818,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 +5876,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); @@ -5855,6 +6010,19 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getAll', ), + getCurrencyRate: (currency) => { + const rate = this.multichainRatesController.state.rates[currency]; + const { fiatCurrency } = this.multichainRatesController.state; + + if (!rate) { + return undefined; + } + + return { + ...rate, + currency: fiatCurrency, + }; + }, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hasPermission: this.permissionController.hasPermission.bind( this.permissionController, @@ -6083,7 +6251,7 @@ export default class MetamaskController extends EventEmitter { return; } - this.accountTracker.syncWithAddresses(addresses); + this.accountTrackerController.syncWithAddresses(addresses); } /** @@ -6170,6 +6338,23 @@ export default class MetamaskController extends EventEmitter { }); } + /** + * The chain list is fetched live at runtime, falling back to a cache. + * This preseeds the cache at startup with a static list provided at build. + */ + async initializeChainlist() { + const cacheKey = `cachedFetch:${CHAIN_SPEC_URL}`; + const { cachedResponse } = (await getStorageItem(cacheKey)) || {}; + if (cachedResponse) { + return; + } + await setStorageItem(cacheKey, { + cachedResponse: rawChainData(), + // Cached value is immediately invalidated + cachedTime: 0, + }); + } + /** * Returns the nonce that will be associated with a transaction once approved * @@ -6348,7 +6533,7 @@ export default class MetamaskController extends EventEmitter { return null; } const { knownMethodData, use4ByteResolution } = - this.preferencesController.store.getState(); + this.preferencesController.state; const prefixedData = addHexPrefix(data); return getMethodDataName( knownMethodData, @@ -6361,11 +6546,11 @@ export default class MetamaskController extends EventEmitter { ); }, getIsRedesignedConfirmationsDeveloperEnabled: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .isRedesignedConfirmationsDeveloperEnabled; }, getIsConfirmationAdvancedDetailsOpen: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .showConfirmationAdvancedDetails; }, }; @@ -6592,7 +6777,7 @@ export default class MetamaskController extends EventEmitter { try { this.approvalController.reject( id, - new EthereumRpcError(error.code, error.message, error.data), + new JsonRpcError(error.code, error.message, error.data), ); } catch (exp) { if (!(exp instanceof ApprovalRequestNotFoundError)) { @@ -6961,30 +7146,6 @@ export default class MetamaskController extends EventEmitter { }; } - async #onPreferencesControllerStateChange(currentState, previousState) { - const { currentLocale } = currentState; - const chainId = getCurrentChainId({ - metamask: this.networkController.state, - }); - - await updateCurrentLocale(currentLocale); - - if (currentState.incomingTransactionsPreferences?.[chainId]) { - this.txController.startIncomingTransactionPolling(); - } else { - this.txController.stopIncomingTransactionPolling(); - } - - this.#checkTokenListPolling(currentState, previousState); - - // TODO: Remove once the preferences controller has been replaced with the core monorepo implementation - this.controllerMessenger.publish( - 'PreferencesController:stateChange', - currentState, - [], - ); - } - #checkTokenListPolling(currentState, previousState) { const previousEnabled = this.#isTokenListPollingRequired(previousState); const newEnabled = this.#isTokenListPollingRequired(currentState); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 4121160a45af..750f8771568b 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -45,7 +45,9 @@ import { } from './lib/accounts/BalancesController'; import { BalancesTracker as MultichainBalancesTracker } from './lib/accounts/BalancesTracker'; import { deferredPromise } from './lib/util'; -import MetaMaskController from './metamask-controller'; +import MetaMaskController, { + ONE_KEY_VIA_TREZOR_MINOR_VERSION, +} from './metamask-controller'; const { Ganache } = require('../../test/e2e/seeder/ganache'); @@ -114,22 +116,6 @@ const rpcMethodMiddlewareMock = { }; jest.mock('./lib/rpc-method-middleware', () => rpcMethodMiddlewareMock); -jest.mock( - './controllers/preferences-controller', - () => - function (...args) { - const PreferencesController = jest.requireActual( - './controllers/preferences-controller', - ).default; - const controller = new PreferencesController(...args); - // jest.spyOn gets hoisted to the top of this function before controller is initialized. - // This forces us to replace the function directly with a jest stub instead. - // eslint-disable-next-line jest/prefer-spy-on - controller.store.subscribe = jest.fn(); - return controller; - }, -); - const KNOWN_PUBLIC_KEY = '02065bc80d3d12b3688e4ad5ab1e9eda6adf24aec2518bfc21b87c99d4c5077ab0'; @@ -357,10 +343,10 @@ describe('MetaMaskController', () => { let metamaskController; async function simulatePreferencesChange(preferences) { - metamaskController.preferencesController.store.subscribe.mock.lastCall[0]( + metamaskController.controllerMessenger.publish( + 'PreferencesController:stateChange', preferences, ); - await flushPromises(); } @@ -517,6 +503,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'; @@ -559,8 +590,7 @@ describe('MetaMaskController', () => { await localMetaMaskController.submitPassword(password); const identities = Object.keys( - localMetaMaskController.preferencesController.store.getState() - .identities, + localMetaMaskController.preferencesController.state.identities, ); const addresses = await localMetaMaskController.keyringController.getAccounts(); @@ -738,19 +768,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 +792,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, @@ -858,6 +896,73 @@ describe('MetaMaskController', () => { ); }); + describe('getHardwareDeviceName', () => { + const hdPath = "m/44'/60'/0'/0/0"; + + it('should return the correct device name for Ledger', async () => { + const deviceName = 'ledger'; + + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('ledger'); + }); + + it('should return the correct device name for Lattice', async () => { + const deviceName = 'lattice'; + + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('lattice'); + }); + + it('should return the correct device name for Trezor', async () => { + const deviceName = 'trezor'; + jest + .spyOn(metamaskController, 'getKeyringForDevice') + .mockResolvedValue({ + bridge: { + minorVersion: 1, + model: 'T', + }, + }); + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('trezor'); + }); + + it('should return undefined for unknown device name', async () => { + const deviceName = 'unknown'; + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe(deviceName); + }); + + it('should handle special case for OneKeyDevice via Trezor', async () => { + const deviceName = 'trezor'; + jest + .spyOn(metamaskController, 'getKeyringForDevice') + .mockResolvedValue({ + bridge: { + model: 'T', + minorVersion: ONE_KEY_VIA_TREZOR_MINOR_VERSION, + }, + }); + const result = await metamaskController.getDeviceNameForMetric( + deviceName, + hdPath, + ); + expect(result).toBe('OneKey via Trezor'); + }); + }); + describe('forgetDevice', () => { it('should throw if it receives an unknown device name', async () => { const result = metamaskController.forgetDevice( @@ -884,8 +989,7 @@ describe('MetaMaskController', () => { expect( Object.keys( - metamaskController.preferencesController.store.getState() - .identities, + metamaskController.preferencesController.state.identities, ), ).not.toContain(hardwareKeyringAccount); expect( @@ -1687,21 +1791,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 +1824,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 +1848,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 +1875,7 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); 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/131.test.ts b/app/scripts/migrations/131.test.ts new file mode 100644 index 000000000000..ab359ff7283c --- /dev/null +++ b/app/scripts/migrations/131.test.ts @@ -0,0 +1,244 @@ +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { cloneDeep } from 'lodash'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; +import { migrate, version } from './131'; + +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + +const oldVersion = 130; + +const mockInternalAccount = createMockInternalAccount(); +const mockAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: { + [mockInternalAccount.id]: mockInternalAccount, + }, + selectedAccount: mockInternalAccount.id, + }, +}; + +describe(`migration #${version}`, () => { + afterEach(() => jest.resetAllMocks()); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('updates selected account if it is not found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: 'unknown id', + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data.AccountsController).toStrictEqual({ + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: mockInternalAccount.id, + }, + }); + }); + + it('does nothing if the selectedAccount is found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if AccountsController state is missing', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + OtherController: {}, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('sets selected account to default state if there are no accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + accounts: {}, + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data.AccountsController).toStrictEqual({ + ...oldStorage.data.AccountsController, + internalAccounts: { + ...oldStorage.data.AccountsController.internalAccounts, + selectedAccount: '', + }, + }); + }); + + it('does nothing if selectedAccount is unset', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: '', + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + const invalidState = [ + { + errorMessage: `Migration ${version}: Invalid AccountsController state of type 'string'`, + label: 'AccountsController type', + state: { AccountsController: 'invalid' }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController state, missing internalAccounts`, + label: 'Missing internalAccounts', + state: { AccountsController: {} }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state of type 'string'`, + label: 'Invalid internalAccounts', + state: { AccountsController: { internalAccounts: 'invalid' } }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state, missing selectedAccount`, + label: 'Missing selectedAccount', + state: { AccountsController: { internalAccounts: { accounts: {} } } }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.selectedAccount state of type 'object'`, + label: 'Invalid selectedAccount', + state: { + AccountsController: { + internalAccounts: { accounts: {}, selectedAccount: {} }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state, missing accounts`, + label: 'Missing accounts', + state: { + AccountsController: { + internalAccounts: { selectedAccount: '' }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state of type 'string'`, + label: 'Invalid accounts', + state: { + AccountsController: { + internalAccounts: { accounts: 'invalid', selectedAccount: '' }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found of type 'string'`, + label: 'Invalid accounts entry', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: 'invalid' }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found that is missing an id`, + label: 'Missing ID in accounts entry', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: {} }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found with an id of type 'object'`, + label: 'Invalid ID for accounts entry', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: { id: {} } }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + ]; + + // @ts-expect-error 'each' function missing from type definitions, but it does exist + it.each(invalidState)( + 'captures error when state is invalid due to: $label', + async ({ + errorMessage, + state, + }: { + errorMessage: string; + state: Record; + }) => { + const oldStorage = { + meta: { version: oldVersion }, + data: state, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(errorMessage), + ); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }, + ); +}); diff --git a/app/scripts/migrations/131.ts b/app/scripts/migrations/131.ts new file mode 100644 index 000000000000..9d2ebf970fbd --- /dev/null +++ b/app/scripts/migrations/131.ts @@ -0,0 +1,147 @@ +import { hasProperty } from '@metamask/utils'; +import { cloneDeep, isObject } from 'lodash'; +import log from 'loglevel'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 131; + +/** + * Fix AccountsController state corruption, where the `selectedAccount` state is set to an invalid + * ID. + * + * @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): void { + if (!hasProperty(state, 'AccountsController')) { + return; + } + + const accountsControllerState = state.AccountsController; + + if (!isObject(accountsControllerState)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController state of type '${typeof accountsControllerState}'`, + ), + ); + return; + } else if (!hasProperty(accountsControllerState, 'internalAccounts')) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController state, missing internalAccounts`, + ), + ); + return; + } else if (!isObject(accountsControllerState.internalAccounts)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state of type '${typeof accountsControllerState.internalAccounts}'`, + ), + ); + return; + } else if ( + !hasProperty(accountsControllerState.internalAccounts, 'selectedAccount') + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state, missing selectedAccount`, + ), + ); + return; + } else if ( + typeof accountsControllerState.internalAccounts.selectedAccount !== 'string' + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.selectedAccount state of type '${typeof accountsControllerState + .internalAccounts.selectedAccount}'`, + ), + ); + return; + } else if ( + !hasProperty(accountsControllerState.internalAccounts, 'accounts') + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state, missing accounts`, + ), + ); + return; + } else if (!isObject(accountsControllerState.internalAccounts.accounts)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state of type '${typeof accountsControllerState + .internalAccounts.accounts}'`, + ), + ); + return; + } + + if ( + Object.keys(accountsControllerState.internalAccounts.accounts).length === 0 + ) { + // In this case since there aren't any accounts, we set the selected account to the default state to unblock the extension. + accountsControllerState.internalAccounts.selectedAccount = ''; + return; + } else if (accountsControllerState.internalAccounts.selectedAccount === '') { + log.warn(`Migration ${version}: Skipping, no selected account set`); + return; + } + + // Safe to use index 0, we already check for the length before. + const firstAccount = Object.values( + accountsControllerState.internalAccounts.accounts, + )[0]; + if (!isObject(firstAccount)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found of type '${typeof firstAccount}'`, + ), + ); + return; + } else if (!hasProperty(firstAccount, 'id')) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found that is missing an id`, + ), + ); + return; + } else if (typeof firstAccount.id !== 'string') { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found with an id of type '${typeof firstAccount.id}'`, + ), + ); + return; + } + + // If the currently selected account ID is not on the `accounts` object, then + // we fallback to first account of the wallet. + if ( + !hasProperty( + accountsControllerState.internalAccounts.accounts, + accountsControllerState.internalAccounts.selectedAccount, + ) + ) { + accountsControllerState.internalAccounts.selectedAccount = firstAccount.id; + } +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 11b55e1eeefb..d2c63eb2e35c 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -148,6 +148,10 @@ const migrations = [ require('./126'), require('./126.1'), require('./127'), + require('./128'), + require('./129'), + require('./130'), + require('./131'), ]; export default migrations; diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts index 50656c7583d5..c725a2cbd837 100644 --- a/app/scripts/snaps/preinstalled-snaps.ts +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -9,11 +9,10 @@ import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/pr // The casts here are less than ideal but we expect the SnapController to validate the inputs. const PREINSTALLED_SNAPS = Object.freeze([ - MessageSigningSnap as PreinstalledSnap, + MessageSigningSnap as unknown as PreinstalledSnap, EnsResolverSnap as PreinstalledSnap, ///: BEGIN:ONLY_INCLUDE_IF(build-flask) AccountWatcherSnap as PreinstalledSnap, - AccountWatcherSnap as PreinstalledSnap, BitcoinWalletSnap as unknown as PreinstalledSnap, PreinstalledExampleSnap as unknown as PreinstalledSnap, ///: END:ONLY_INCLUDE_IF diff --git a/builds.yml b/builds.yml index acee49063822..bcd035b56bc1 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -67,7 +67,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -90,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io @@ -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/fitness-functions/common/constants.test.ts b/development/fitness-functions/common/constants.test.ts index 21912d5fa194..e0077f086594 100644 --- a/development/fitness-functions/common/constants.test.ts +++ b/development/fitness-functions/common/constants.test.ts @@ -1,8 +1,34 @@ -import { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX } from './constants'; +import { E2E_TESTS_REGEX, JS_REGEX } from './constants'; describe('Regular Expressions used in Fitness Functions', (): void => { - describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, (): void => { + describe(`E2E_TESTS_REGEX "${E2E_TESTS_REGEX}"`, (): void => { const PATHS_IT_SHOULD_MATCH = [ + // JS, TS, JSX, and TSX files inside the + // test/e2e directory + 'test/e2e/file.js', + 'test/e2e/path/file.ts', + 'test/e2e/much/longer/path/file.jsx', + 'test/e2e/much/longer/path/file.tsx', + // development/fitness-functions directory + 'development/fitness-functions/file.js', + 'development/fitness-functions/path/file.ts', + 'development/fitness-functions/much/longer/path/file.jsx', + 'development/fitness-functions/much/longer/path/file.tsx', + // development/webpack directory + 'development/webpack/file.js', + 'development/webpack/path/file.ts', + 'development/webpack/much/longer/path/file.jsx', + 'development/webpack/much/longer/path/file.tsx', + ]; + + const PATHS_IT_SHOULD_NOT_MATCH = [ + // any files without JS, TS, JSX or TSX extension + 'file', + 'file.extension', + 'path/file.extension', + 'much/longer/path/file.extension', + // JS, TS, JSX, and TSX files outside + // the test/e2e, development/fitness-functions, development/webpack directories 'file.js', 'path/file.js', 'much/longer/path/file.js', @@ -12,39 +38,15 @@ describe('Regular Expressions used in Fitness Functions', (): void => { 'file.jsx', 'path/file.jsx', 'much/longer/path/file.jsx', - ]; - - const PATHS_IT_SHOULD_NOT_MATCH = [ - // any without JS, TS, JSX or TSX extension - 'file', - 'file.extension', - 'path/file.extension', - 'much/longer/path/file.extension', - // any in the test/e2e directory - 'test/e2e/file', - 'test/e2e/file.extension', - 'test/e2e/path/file.extension', - 'test/e2e/much/longer/path/file.extension', - 'test/e2e/file.js', - 'test/e2e/path/file.ts', - 'test/e2e/much/longer/path/file.jsx', - 'test/e2e/much/longer/path/file.tsx', - // any in the development/fitness-functions directory - 'development/fitness-functions/file', - 'development/fitness-functions/file.extension', - 'development/fitness-functions/path/file.extension', - 'development/fitness-functions/much/longer/path/file.extension', - 'development/fitness-functions/file.js', - 'development/fitness-functions/path/file.ts', - 'development/fitness-functions/much/longer/path/file.jsx', - 'development/fitness-functions/much/longer/path/file.tsx', + 'file.tsx', + 'path/file.tsx', + 'much/longer/path/file.tsx', ]; describe('included paths', (): void => { PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { it(`should match "${path}"`, (): void => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - + const result = E2E_TESTS_REGEX.test(path); expect(result).toStrictEqual(true); }); }); @@ -53,22 +55,23 @@ describe('Regular Expressions used in Fitness Functions', (): void => { describe('excluded paths', (): void => { PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { it(`should not match "${path}"`, (): void => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - + const result = E2E_TESTS_REGEX.test(path); expect(result).toStrictEqual(false); }); }); }); }); - describe(`SHARED_FOLDER_JS_REGEX "${SHARED_FOLDER_JS_REGEX}"`, (): void => { + describe(`JS_REGEX "${JS_REGEX}"`, (): void => { const PATHS_IT_SHOULD_MATCH = [ + 'app/much/longer/path/file.js', + 'app/much/longer/path/file.jsx', + 'offscreen/path/file.js', + 'offscreen/path/file.jsx', 'shared/file.js', - 'shared/path/file.js', - 'shared/much/longer/path/file.js', 'shared/file.jsx', - 'shared/path/file.jsx', - 'shared/much/longer/path/file.jsx', + 'ui/much/longer/path/file.js', + 'ui/much/longer/path/file.jsx', ]; const PATHS_IT_SHOULD_NOT_MATCH = [ @@ -80,13 +83,15 @@ describe('Regular Expressions used in Fitness Functions', (): void => { 'file.ts', 'path/file.ts', 'much/longer/path/file.tsx', + // any JS or JSX files outside the app, offscreen, shared, and ui directories + 'test/longer/path/file.js', + 'random/longer/path/file.jsx', ]; describe('included paths', (): void => { PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { it(`should match "${path}"`, (): void => { - const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); - + const result = JS_REGEX.test(path); expect(result).toStrictEqual(true); }); }); @@ -95,8 +100,7 @@ describe('Regular Expressions used in Fitness Functions', (): void => { describe('excluded paths', (): void => { PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { it(`should not match "${path}"`, (): void => { - const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); - + const result = JS_REGEX.test(path); expect(result).toStrictEqual(false); }); }); diff --git a/development/fitness-functions/common/constants.ts b/development/fitness-functions/common/constants.ts index 5758d4e2a6e1..f3996a294a5a 100644 --- a/development/fitness-functions/common/constants.ts +++ b/development/fitness-functions/common/constants.ts @@ -1,10 +1,12 @@ -// include JS, TS, JSX, TSX files only excluding files in the e2e tests and -// fitness functions directories -const EXCLUDE_E2E_TESTS_REGEX = - '^(?!test/e2e)(?!development/fitness|development/webpack).*.(js|ts|jsx|tsx)$'; +// include JS, TS, JSX, TSX files only in the +// test/e2e +// development/fitness-functions +// development/webpack directories +const E2E_TESTS_REGEX = + /^(test\/e2e|development\/fitness-functions|development\/webpack).*\.(js|ts|jsx|tsx)$/u; -// include JS and JSX files in the shared directory only -const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$'; +// include JS and JSX files only in the app, offscreen, shared, and ui directories +const JS_REGEX = /^(app|offscreen|shared|ui)\/.*\.(js|jsx)$/u; enum AUTOMATION_TYPE { CI = 'ci', @@ -12,4 +14,4 @@ enum AUTOMATION_TYPE { PRE_PUSH_HOOK = 'pre-push-hook', } -export { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX, AUTOMATION_TYPE }; +export { E2E_TESTS_REGEX, JS_REGEX, AUTOMATION_TYPE }; diff --git a/development/fitness-functions/common/shared.test.ts b/development/fitness-functions/common/shared.test.ts index 92306b9d4751..66af337fbfc8 100644 --- a/development/fitness-functions/common/shared.test.ts +++ b/development/fitness-functions/common/shared.test.ts @@ -30,13 +30,13 @@ describe('filterDiffFileCreations()', (): void => { const actualResult = filterDiffFileCreations(testFileDiff); expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js - new file mode 100644 - index 000000000..30d74d258 - --- /dev/null - +++ b/old-file.js - @@ -0,0 +1 @@ - +ping" + "diff --git a/old-file.js b/old-file.js + new file mode 100644 + index 000000000..30d74d258 + --- /dev/null + +++ b/old-file.js + @@ -0,0 +1 @@ + +ping" `); }); }); @@ -44,9 +44,9 @@ describe('filterDiffFileCreations()', (): void => { describe('hasNumberOfCodeBlocksIncreased()', (): void => { it('should show which code blocks have increased', (): void => { const testDiffFragment = ` - +foo - +bar - +baz`; + +foo + +bar + +baz`; const testCodeBlocks = ['code block 1', 'foo', 'baz']; const actualResult = hasNumberOfCodeBlocksIncreased( @@ -69,7 +69,7 @@ describe('filterDiffByFilePath()', (): void => { it('should return the right diff for a generic matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, - '.*/.*.(js|ts)$|.*.(js|ts)$', + /^(.*\/)?.*\.(jsx)$/u, // Exclude jsx files ); expect(actualResult).toMatchInlineSnapshot(` @@ -93,35 +93,17 @@ describe('filterDiffByFilePath()', (): void => { }); it('should return the right diff for a specific file in any dir matcher', (): void => { - const actualResult = filterDiffByFilePath(testFileDiff, '.*old-file.js$'); + const actualResult = filterDiffByFilePath(testFileDiff, /.*old-file\.js$/u); // Exclude old-file.js expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js - index 57d5de75c..808d8ba37 100644 - --- a/old-file.js - +++ b/old-file.js - @@ -1,3 +1,8 @@ - +ping - @@ -34,33 +39,4 @@ - -pong" - `); - }); - - it('should return the right diff for a multiple file extension (OR) matcher', (): void => { - const actualResult = filterDiffByFilePath( - testFileDiff, - '^(./)*old-file.(js|ts|jsx)$', - ); - - expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js + "diff --git a/new-file.ts b/new-file.ts index 57d5de75c..808d8ba37 100644 - --- a/old-file.js - +++ b/old-file.js + --- a/new-file.ts + +++ b/new-file.ts @@ -1,3 +1,8 @@ - +ping + +foo @@ -34,33 +39,4 @@ - -pong + -bar diff --git a/old-file.jsx b/old-file.jsx index 57d5de75c..808d8ba37 100644 --- a/old-file.jsx @@ -133,10 +115,10 @@ describe('filterDiffByFilePath()', (): void => { `); }); - it('should return the right diff for a file name negation matcher', (): void => { + it('should return the right diff for a multiple file extension (OR) matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, - '^(?!.*old-file.js$).*.[a-zA-Z]+$', + /^(\.\/)*old-file\.(js|ts|jsx)$/u, // Exclude files named old-file that have js, ts, or jsx extensions ); expect(actualResult).toMatchInlineSnapshot(` @@ -147,15 +129,25 @@ describe('filterDiffByFilePath()', (): void => { @@ -1,3 +1,8 @@ +foo @@ -34,33 +39,4 @@ - -bar - diff --git a/old-file.jsx b/old-file.jsx - index 57d5de75c..808d8ba37 100644 - --- a/old-file.jsx - +++ b/old-file.jsx - @@ -1,3 +1,8 @@ - +yin - @@ -34,33 +39,4 @@ - -yang" + -bar" `); }); + + it('should return the right diff for a file name negation matcher', (): void => { + const actualResult = filterDiffByFilePath( + testFileDiff, + /^(?!.*old-file\.js$).*\.[a-zA-Z]+$/u, // Exclude files that do not end with old-file.js but include all other file extensions + ); + + expect(actualResult).toMatchInlineSnapshot(` + "diff --git a/old-file.js b/old-file.js + index 57d5de75c..808d8ba37 100644 + --- a/old-file.js + +++ b/old-file.js + @@ -1,3 +1,8 @@ + +ping + @@ -34,33 +39,4 @@ + -pong" + `); + }); }); diff --git a/development/fitness-functions/common/shared.ts b/development/fitness-functions/common/shared.ts index f7f22101378d..e96073ab5b27 100644 --- a/development/fitness-functions/common/shared.ts +++ b/development/fitness-functions/common/shared.ts @@ -1,11 +1,11 @@ -function filterDiffByFilePath(diff: string, regex: string): string { +function filterDiffByFilePath(diff: string, regex: RegExp): string { // split by `diff --git` and remove the first element which is empty const diffBlocks = diff.split(`diff --git`).slice(1); const filteredDiff = diffBlocks .map((block) => block.trim()) .filter((block) => { - let didAPathInBlockMatchRegEx = false; + let shouldCheckBlock = false; block // get the first line of the block which has the paths @@ -18,12 +18,13 @@ function filterDiffByFilePath(diff: string, regex: string): string { // if at least one of the two paths matches the regex, filter the // corresponding diff block in .forEach((path) => { - if (new RegExp(regex, 'u').test(path)) { - didAPathInBlockMatchRegEx = true; + if (!regex.test(path)) { + // Not excluded, include in check + shouldCheckBlock = true; } }); - return didAPathInBlockMatchRegEx; + return shouldCheckBlock; }) // prepend `git --diff` to each block .map((block) => `diff --git ${block}`) @@ -32,6 +33,34 @@ function filterDiffByFilePath(diff: string, regex: string): string { return filteredDiff; } +function restrictedFilePresent(diff: string, regex: RegExp): boolean { + // split by `diff --git` and remove the first element which is empty + const diffBlocks = diff.split(`diff --git`).slice(1); + let jsOrJsxFilePresent = false; + diffBlocks + .map((block) => block.trim()) + .filter((block) => { + block + // get the first line of the block which has the paths + .split('\n')[0] + .trim() + // split the two paths + .split(' ') + // remove `a/` and `b/` from the paths + .map((path) => path.substring(2)) + // if at least one of the two paths matches the regex, filter the + // corresponding diff block in + .forEach((path) => { + if (regex.test(path)) { + // Not excluded, include in check + jsOrJsxFilePresent = true; + } + }); + return jsOrJsxFilePresent; + }); + return jsOrJsxFilePresent; +} + // This function returns all lines that are additions to files that are being // modified but that previously already existed. Example: // diff --git a/test.js b/test.js @@ -44,7 +73,9 @@ function filterDiffLineAdditions(diff: string): string { const diffLines = diff.split('\n'); const diffAdditionLines = diffLines.filter((line) => { - const isAdditionLine = line.startsWith('+') && !line.startsWith('+++'); + const trimmedLine = line.trim(); + const isAdditionLine = + trimmedLine.startsWith('+') && !trimmedLine.startsWith('+++'); return isAdditionLine; }); @@ -108,6 +139,7 @@ function hasNumberOfCodeBlocksIncreased( export { filterDiffByFilePath, + restrictedFilePresent, filterDiffFileCreations, filterDiffLineAdditions, hasNumberOfCodeBlocksIncreased, diff --git a/development/fitness-functions/rules/index.ts b/development/fitness-functions/rules/index.ts index cd74d286093d..6ba0f1198684 100644 --- a/development/fitness-functions/rules/index.ts +++ b/development/fitness-functions/rules/index.ts @@ -5,23 +5,25 @@ const RULES: IRule[] = [ { name: "Don't use `sinon` or `assert` in unit tests", fn: preventSinonAssertSyntax, - docURL: - 'https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', + errorMessage: + '`sinon` or `assert` was detected in the diff. Please use Jest instead. For more info: https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', }, { - name: "Don't add JS or JSX files to the `shared` directory", + name: "Don't add JS or JSX files", fn: preventJavaScriptFileAdditions, + errorMessage: + 'The diff includes a newly created JS or JSX file. Please use TS or TSX instead.', }, ]; type IRule = { name: string; fn: (diff: string) => boolean; - docURL?: string; + errorMessage: string; }; function runFitnessFunctionRule(rule: IRule, diff: string): void { - const { name, fn, docURL } = rule; + const { name, fn, errorMessage } = rule; console.log(`Checking rule "${name}"...`); const hasRulePassed: boolean = fn(diff) as boolean; @@ -29,11 +31,7 @@ function runFitnessFunctionRule(rule: IRule, diff: string): void { console.log(`...OK`); } else { console.log(`...FAILED. Changes not accepted by the fitness function.`); - - if (docURL) { - console.log(`For more info: ${docURL}.`); - } - + console.log(errorMessage); process.exit(1); } } diff --git a/development/fitness-functions/rules/javascript-additions.test.ts b/development/fitness-functions/rules/javascript-additions.test.ts index db1803c1d9af..f1ae6e378e37 100644 --- a/development/fitness-functions/rules/javascript-additions.test.ts +++ b/development/fitness-functions/rules/javascript-additions.test.ts @@ -13,11 +13,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(true); }); - it('should pass when receiving a diff with a new TS file on the shared folder', (): void => { + it('should pass when receiving a diff with a new TS file folder', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.ts', 'yada yada yada yada'), + generateCreateFileDiff('app/test.ts', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); @@ -25,11 +25,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(true); }); - it('should not pass when receiving a diff with a new JS file on the shared folder', (): void => { + it('should not pass when receiving a diff with a new JS file', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.js', 'yada yada yada yada'), + generateCreateFileDiff('app/test.js', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); @@ -37,11 +37,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(false); }); - it('should not pass when receiving a diff with a new JSX file on the shared folder', (): void => { + it('should not pass when receiving a diff with a new JSX file', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.jsx', 'yada yada yada yada'), + generateCreateFileDiff('app/test.jsx', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); diff --git a/development/fitness-functions/rules/javascript-additions.ts b/development/fitness-functions/rules/javascript-additions.ts index 3e3705ea30f0..0d7c39b07110 100644 --- a/development/fitness-functions/rules/javascript-additions.ts +++ b/development/fitness-functions/rules/javascript-additions.ts @@ -1,15 +1,12 @@ -import { SHARED_FOLDER_JS_REGEX } from '../common/constants'; +import { JS_REGEX } from '../common/constants'; import { - filterDiffByFilePath, filterDiffFileCreations, + restrictedFilePresent, } from '../common/shared'; function preventJavaScriptFileAdditions(diff: string): boolean { - const sharedFolderDiff = filterDiffByFilePath(diff, SHARED_FOLDER_JS_REGEX); - const sharedFolderCreationDiff = filterDiffFileCreations(sharedFolderDiff); - - const hasCreatedAtLeastOneJSFileInShared = sharedFolderCreationDiff !== ''; - if (hasCreatedAtLeastOneJSFileInShared) { + const diffAdditions = filterDiffFileCreations(diff); + if (restrictedFilePresent(diffAdditions, JS_REGEX)) { return false; } return true; diff --git a/development/fitness-functions/rules/sinon-assert-syntax.ts b/development/fitness-functions/rules/sinon-assert-syntax.ts index 2cc56ec37762..a40c0768ad06 100644 --- a/development/fitness-functions/rules/sinon-assert-syntax.ts +++ b/development/fitness-functions/rules/sinon-assert-syntax.ts @@ -1,4 +1,4 @@ -import { EXCLUDE_E2E_TESTS_REGEX } from '../common/constants'; +import { E2E_TESTS_REGEX } from '../common/constants'; import { filterDiffByFilePath, filterDiffFileCreations, @@ -15,7 +15,7 @@ const codeBlocks = [ ]; function preventSinonAssertSyntax(diff: string): boolean { - const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX); + const diffByFilePath = filterDiffByFilePath(diff, E2E_TESTS_REGEX); const diffAdditions = filterDiffFileCreations(diffByFilePath); const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks); diff --git a/development/sentry-publish.js b/development/sentry-publish.js index 22ff6156a243..5b60b5bd6a4f 100755 --- a/development/sentry-publish.js +++ b/development/sentry-publish.js @@ -1,5 +1,7 @@ #!/usr/bin/env node +const fs = require('node:fs/promises'); +const path = require('node:path'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); @@ -80,17 +82,19 @@ async function start() { ]); } - const additionalUploadArgs = []; - if (dist) { - additionalUploadArgs.push('--dist', dist); - } + let distDirectory = 'dist'; if (buildType !== loadBuildTypesConfig().default) { - additionalUploadArgs.push( - '--dist-directory', - dist ? `dist-${buildType}-${dist}` : `dist-${buildType}`, - ); + distDirectory = dist ? `dist-${buildType}-${dist}` : `dist-${buildType}`; } else if (dist) { - additionalUploadArgs.push('--dist-directory', `dist-${dist}`); + distDirectory = `dist-${dist}`; + } + + const absoluteDistDirectory = path.resolve(__dirname, '../', distDirectory); + await assertIsNonEmptyDirectory(absoluteDistDirectory); + + const additionalUploadArgs = ['--dist-directory', distDirectory]; + if (dist) { + additionalUploadArgs.push('--dist', dist); } // upload sentry source and sourcemaps await runInShell('./development/sentry-upload-artifacts.sh', [ @@ -123,3 +127,38 @@ async function doesNotFail(asyncFn) { throw error; } } + +/** + * Assert that the given path exists, and is a non-empty directory. + * + * @param {string} directoryPath - The path to check. + */ +async function assertIsNonEmptyDirectory(directoryPath) { + await assertIsDirectory(directoryPath); + + const files = await fs.readdir(directoryPath); + if (!files.length) { + throw new Error(`Directory empty: '${directoryPath}'`); + } +} + +/** + * Assert that the given path exists, and is a directory. + * + * @param {string} directoryPath - The path to check. + */ +async function assertIsDirectory(directoryPath) { + try { + const directoryStats = await fs.stat(directoryPath); + if (!directoryStats.isDirectory()) { + throw new Error(`Invalid path '${directoryPath}'; must be a directory`); + } + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`Directory '${directoryPath}' not found`, { + cause: error, + }); + } + throw error; + } +} diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index d5063250db16..5de1f953bb87 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -4,8 +4,6 @@ "app/scripts/constants/contracts.js", "app/scripts/constants/on-ramp.js", "app/scripts/contentscript.js", - "app/scripts/controllers/alert.js", - "app/scripts/controllers/app-state.js", "app/scripts/controllers/cached-balances.js", "app/scripts/controllers/cached-balances.test.js", "app/scripts/controllers/ens/ens.js", @@ -40,8 +38,6 @@ "app/scripts/controllers/permissions/selectors.test.js", "app/scripts/controllers/permissions/specifications.js", "app/scripts/controllers/permissions/specifications.test.js", - "app/scripts/controllers/preferences.js", - "app/scripts/controllers/preferences.test.js", "app/scripts/controllers/swaps.js", "app/scripts/controllers/swaps.test.js", "app/scripts/controllers/transactions/index.js", @@ -63,7 +59,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 +1411,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", @@ -1503,10 +1495,6 @@ "ui/pages/swaps/awaiting-swap/swap-failure-icon.test.js", "ui/pages/swaps/awaiting-swap/swap-success-icon.js", "ui/pages/swaps/awaiting-swap/swap-success-icon.test.js", - "ui/pages/swaps/build-quote/build-quote.js", - "ui/pages/swaps/build-quote/build-quote.stories.js", - "ui/pages/swaps/build-quote/build-quote.test.js", - "ui/pages/swaps/build-quote/index.js", "ui/pages/swaps/countdown-timer/countdown-timer.js", "ui/pages/swaps/countdown-timer/countdown-timer.stories.js", "ui/pages/swaps/countdown-timer/countdown-timer.test.js", @@ -1516,14 +1504,6 @@ "ui/pages/swaps/create-new-swap/create-new-swap.js", "ui/pages/swaps/create-new-swap/create-new-swap.test.js", "ui/pages/swaps/create-new-swap/index.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js", - "ui/pages/swaps/dropdown-input-pair/index.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js", - "ui/pages/swaps/dropdown-search-list/index.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.stories.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.test.js", @@ -1548,12 +1528,6 @@ "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.test.js", "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js", "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.test.js", - "ui/pages/swaps/main-quote-summary/index.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.test.js", - "ui/pages/swaps/main-quote-summary/quote-backdrop.js", - "ui/pages/swaps/main-quote-summary/quote-backdrop.test.js", "ui/pages/swaps/searchable-item-list/index.js", "ui/pages/swaps/searchable-item-list/item-list/index.js", "ui/pages/swaps/searchable-item-list/item-list/item-list.component.js", @@ -1574,10 +1548,6 @@ "ui/pages/swaps/select-quote-popover/sort-list/index.js", "ui/pages/swaps/select-quote-popover/sort-list/sort-list.js", "ui/pages/swaps/select-quote-popover/sort-list/sort-list.test.js", - "ui/pages/swaps/slippage-buttons/index.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.test.js", "ui/pages/swaps/smart-transaction-status/arrow-icon.js", "ui/pages/swaps/smart-transaction-status/arrow-icon.test.js", "ui/pages/swaps/smart-transaction-status/canceled-icon.js", @@ -1603,11 +1573,6 @@ "ui/pages/swaps/view-on-block-explorer/index.js", "ui/pages/swaps/view-on-block-explorer/view-on-block-explorer.js", "ui/pages/swaps/view-on-block-explorer/view-on-block-explorer.test.js", - "ui/pages/swaps/view-quote/index.js", - "ui/pages/swaps/view-quote/view-quote-price-difference.js", - "ui/pages/swaps/view-quote/view-quote-price-difference.test.js", - "ui/pages/swaps/view-quote/view-quote.js", - "ui/pages/swaps/view-quote/view-quote.test.js", "ui/pages/token-details/index.js", "ui/pages/token-details/token-details-page.js", "ui/pages/token-details/token-details-page.test.js", diff --git a/docs/confirmations.md b/docs/confirmations.md index 7af838d2053e..86577fc8f691 100644 --- a/docs/confirmations.md +++ b/docs/confirmations.md @@ -168,7 +168,7 @@ function getValues(pendingApproval, t, actions, _history) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: true, }; @@ -401,4 +401,4 @@ When an approval flow is created, this is reflected in the state and the UI will ### Custom Success Approval -[](assets/confirmation.png) \ No newline at end of file +[](assets/confirmation.png) 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 0ce8a5ab4899..9cbdda6ac03e 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -698,14 +704,14 @@ "@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/assets-controllers>@metamask/rpc-errors": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -715,14 +721,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,11 +728,17 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -859,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -894,11 +919,24 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -1314,6 +1352,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1513,10 +1558,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1559,8 +1604,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1572,11 +1617,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1603,8 +1669,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "bn.js": true, "pify": true } @@ -1626,12 +1692,38 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1840,10 +1932,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1856,6 +1948,49 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1989,6 +2124,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, @@ -2056,10 +2197,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2070,6 +2211,49 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2091,8 +2275,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2103,6 +2287,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2120,8 +2325,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2164,15 +2369,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, @@ -2270,11 +2476,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2324,6 +2530,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2409,11 +2621,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2446,8 +2658,8 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2468,15 +2680,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2534,8 +2752,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2550,10 +2768,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2567,17 +2785,30 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2591,6 +2822,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2607,10 +2844,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2640,20 +2877,33 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2726,8 +2976,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2754,6 +3004,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2763,9 +3019,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2782,6 +3038,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4027,11 +4304,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4469,26 +4741,20 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, @@ -4559,6 +4825,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 @@ -5046,37 +5339,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, @@ -5093,13 +5408,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 0ce8a5ab4899..9cbdda6ac03e 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -698,14 +704,14 @@ "@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/assets-controllers>@metamask/rpc-errors": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -715,14 +721,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,11 +728,17 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -859,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -894,11 +919,24 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -1314,6 +1352,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1513,10 +1558,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1559,8 +1604,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1572,11 +1617,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1603,8 +1669,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "bn.js": true, "pify": true } @@ -1626,12 +1692,38 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1840,10 +1932,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1856,6 +1948,49 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1989,6 +2124,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, @@ -2056,10 +2197,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2070,6 +2211,49 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2091,8 +2275,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2103,6 +2287,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2120,8 +2325,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2164,15 +2369,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, @@ -2270,11 +2476,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2324,6 +2530,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2409,11 +2621,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2446,8 +2658,8 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2468,15 +2680,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2534,8 +2752,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2550,10 +2768,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2567,17 +2785,30 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2591,6 +2822,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2607,10 +2844,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2640,20 +2877,33 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2726,8 +2976,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2754,6 +3004,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2763,9 +3019,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2782,6 +3038,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4027,11 +4304,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4469,26 +4741,20 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, @@ -4559,6 +4825,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 @@ -5046,37 +5339,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, @@ -5093,13 +5408,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 0ce8a5ab4899..9cbdda6ac03e 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -698,14 +704,14 @@ "@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/assets-controllers>@metamask/rpc-errors": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -715,14 +721,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,11 +728,17 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -859,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -894,11 +919,24 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -1314,6 +1352,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1513,10 +1558,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1559,8 +1604,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1572,11 +1617,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1603,8 +1669,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "bn.js": true, "pify": true } @@ -1626,12 +1692,38 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1840,10 +1932,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1856,6 +1948,49 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1989,6 +2124,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, @@ -2056,10 +2197,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2070,6 +2211,49 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2091,8 +2275,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2103,6 +2287,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2120,8 +2325,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2164,15 +2369,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, @@ -2270,11 +2476,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2324,6 +2530,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2409,11 +2621,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2446,8 +2658,8 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2468,15 +2680,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2534,8 +2752,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2550,10 +2768,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2567,17 +2785,30 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2591,6 +2822,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2607,10 +2844,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2640,20 +2877,33 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2726,8 +2976,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2754,6 +3004,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2763,9 +3019,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2782,6 +3038,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4027,11 +4304,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4469,26 +4741,20 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, @@ -4559,6 +4825,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 @@ -5046,37 +5339,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, @@ -5093,13 +5408,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 f8cd1fdfca54..fae253f8b9d5 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -754,8 +754,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -766,6 +766,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -790,14 +796,14 @@ "@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/assets-controllers>@metamask/rpc-errors": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -807,14 +813,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,11 +820,17 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -951,11 +955,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -986,11 +1011,24 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/json-rpc-engine": { + "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -1406,6 +1444,13 @@ "jest-canvas-mock>moo-color>color-name": true } }, + "@metamask/json-rpc-engine": { + "packages": { + "@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1605,10 +1650,10 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, @@ -1651,8 +1696,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1664,11 +1709,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1695,8 +1761,8 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "bn.js": true, "pify": true } @@ -1718,12 +1784,38 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/network-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1932,10 +2024,10 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true } @@ -1948,6 +2040,49 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/permission-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2081,6 +2216,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, @@ -2148,10 +2289,10 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true + "@metamask/selected-network-controller": true } }, "@metamask/queued-request-controller>@metamask/base-controller": { @@ -2162,6 +2303,49 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/safe-event-emitter": true + } + }, + "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2183,8 +2367,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2195,6 +2379,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2212,8 +2417,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2256,15 +2461,16 @@ } }, "@metamask/signature-controller": { - "globals": { - "console.info": true - }, "packages": { "@metamask/base-controller": true, "@metamask/controller-utils": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller": true, "@metamask/logging-controller": true, - "@metamask/message-manager": true, - "lodash": true, + "@metamask/message-manager>jsonschema": true, + "@metamask/utils": true, + "browserify>buffer": true, + "uuid": true, "webpack>events": true } }, @@ -2362,11 +2568,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2416,6 +2622,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2501,11 +2713,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2538,8 +2750,8 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/utils": true } }, @@ -2560,15 +2772,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2626,8 +2844,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2642,10 +2860,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2659,17 +2877,30 @@ "immer": true } }, + "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2683,6 +2914,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2699,10 +2936,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2732,20 +2969,33 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": { + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2818,8 +3068,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2846,6 +3096,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2855,9 +3111,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2874,6 +3130,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4119,11 +4396,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4561,26 +4833,20 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, "stream-http": true } }, - "json-rpc-engine": { - "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, @@ -4651,6 +4917,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 @@ -5114,37 +5407,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, @@ -5161,13 +5476,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/build-system/policy.json b/lavamoat/build-system/policy.json index 9bb4675d14c9..e7ce64ceec23 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -160,8 +160,7 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-module-imports": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true, + "depcheck>@babel/traverse": true, "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true } }, @@ -478,7 +477,7 @@ "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-syntax-async-generators": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator": { @@ -494,14 +493,14 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": { "packages": { "@babel/core>@babel/template": true, "@babel/core>@babel/types": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true } }, "@babel/preset-env>@babel/plugin-transform-block-scoped-functions": { @@ -535,12 +534,12 @@ "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-optimise-call-expression": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": true, - "@babel/preset-env>@babel/plugin-transform-classes>globals": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": true, + "@babel/preset-env>@babel/plugin-transform-classes>globals": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": { @@ -548,6 +547,12 @@ "@babel/core>@babel/types": true } }, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": { + "packages": { + "@babel/core>@babel/template": true, + "@babel/core>@babel/types": true + } + }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-optimise-call-expression": { "packages": { "@babel/core>@babel/types": true @@ -558,7 +563,7 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-optimise-call-expression": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": { @@ -566,6 +571,11 @@ "@babel/core>@babel/types": true } }, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": { + "packages": { + "@babel/core>@babel/types": true + } + }, "@babel/preset-env>@babel/plugin-transform-computed-properties": { "packages": { "@babel/core": true, @@ -673,7 +683,7 @@ "packages": { "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true } }, "@babel/preset-env>@babel/plugin-transform-json-strings": { @@ -716,10 +726,15 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": true, + "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": true, "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true } }, + "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": { + "packages": { + "@babel/core>@babel/types": true + } + }, "@babel/preset-env>@babel/plugin-transform-modules-umd": { "builtin": { "path.basename": true, @@ -811,9 +826,7 @@ "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin>semver": true, "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin>semver": { @@ -1042,6 +1055,7 @@ "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin": true, + "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true, "@babel/preset-typescript>@babel/plugin-transform-typescript>@babel/plugin-syntax-typescript": true } }, @@ -1981,7 +1995,7 @@ "Buffer.isBuffer": true }, "packages": { - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true } }, "browserify>string_decoder": { @@ -2346,32 +2360,13 @@ "@babel/code-frame": true, "@babel/core>@babel/generator": true, "@babel/core>@babel/parser": true, + "@babel/core>@babel/template": true, "@babel/core>@babel/types": true, "babel/preset-env>b@babel/types": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true, "depcheck>@babel/traverse>globals": true, "nock>debug": true } }, - "depcheck>@babel/traverse>@babel/helper-function-name": { - "packages": { - "@babel/core>@babel/template": true, - "@babel/core>@babel/types": true - } - }, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": { - "packages": { - "@babel/core>@babel/types": true - } - }, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": { - "packages": { - "@babel/core>@babel/types": true - } - }, "depcheck>cosmiconfig>parse-json": { "packages": { "@babel/code-frame": true, @@ -6333,10 +6328,10 @@ "packages": { "@babel/code-frame": true, "@babel/core>@babel/generator": true, - "depcheck>@babel/traverse>@babel/helper-environment-visitor": true, - "depcheck>@babel/traverse>@babel/helper-function-name": true, - "depcheck>@babel/traverse>@babel/helper-hoist-variables": true, - "depcheck>@babel/traverse>@babel/helper-split-export-declaration": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": true, + "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": true, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>@babel/parser": true, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>@babel/types": true, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>globals": true, diff --git a/offscreen/scripts/trezor.ts b/offscreen/scripts/trezor.ts index e9482a03c575..22e03048fb93 100644 --- a/offscreen/scripts/trezor.ts +++ b/offscreen/scripts/trezor.ts @@ -40,14 +40,17 @@ export default function init() { chrome.runtime.sendMessage({ target: OffscreenCommunicationTarget.extension, event: OffscreenCommunicationEvents.trezorDeviceConnect, - payload: event.payload.features.model, + payload: { + model: event.payload.features.model, + minorVersion: event.payload.features.minor_version, + }, }); } }); TrezorConnectSDK.init({ ...msg.params, - env: 'web', + env: 'webextension', }).then(() => { sendResponse(); }); diff --git a/package.json b/package.json index f1b3fc502664..fec8f2ffb498 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.5.0", + "version": "12.6.0", "private": true, "repository": { "type": "git", @@ -53,7 +53,7 @@ "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask", - "test:e2e:chrome:webpack": "ENABLE_MV3=false SELENIUM_BROWSER=chrome node test/e2e/run-all.js", + "test:e2e:chrome:webpack": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:api-specs": "SELENIUM_BROWSER=chrome ts-node test/e2e/run-openrpc-api-test-coverage.ts", "test:e2e:mmi:ci": "yarn playwright test --project=mmi --project=mmi.visual", "test:e2e:mmi:all": "yarn playwright test --project=mmi && yarn test:e2e:mmi:visual", @@ -229,7 +229,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.5.1", + "@metamask/snaps-sdk": "^6.9.0", "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -253,7 +253,6 @@ "@babel/runtime@npm:^7.8.4": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@spruceid/siwe-parser@npm:1.1.3": "patch:@spruceid/siwe-parser@npm%3A2.1.0#~/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch", "@spruceid/siwe-parser@npm:2.1.0": "patch:@spruceid/siwe-parser@npm%3A2.1.0#~/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch", - "@trezor/connect-web@npm:^9.2.2": "patch:@trezor/connect-web@npm%3A9.2.2#~/.yarn/patches/@trezor-connect-web-npm-9.2.2-a4de8e45fc.patch", "ts-mixer@npm:^6.0.3": "patch:ts-mixer@npm%3A6.0.4#~/.yarn/patches/ts-mixer-npm-6.0.4-5d9747bdf5.patch", "sucrase@npm:3.34.0": "^3.35.0", "@expo/config/glob": "^10.3.10", @@ -262,12 +261,10 @@ "@metamask/message-manager": "^10.1.0", "@metamask/gas-fee-controller@npm:^15.1.1": "patch:@metamask/gas-fee-controller@npm%3A15.1.2#~/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch", "@metamask/nonce-tracker@npm:^5.0.0": "patch:@metamask/nonce-tracker@npm%3A5.0.0#~/.yarn/patches/@metamask-nonce-tracker-npm-5.0.0-d81478218e.patch", - "@trezor/connect-web@npm:^9.1.11": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", "@metamask/network-controller@npm:^17.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^20.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", - "path-to-regexp": "1.9.0", - "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "path-to-regexp": "1.9.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -288,24 +285,24 @@ "@lavamoat/lavadome-react": "0.0.17", "@lavamoat/snow": "^2.0.2", "@material-ui/core": "^4.11.0", - "@metamask-institutional/custody-controller": "^0.2.31", - "@metamask-institutional/custody-keyring": "^2.0.3", - "@metamask-institutional/extension": "^0.3.27", - "@metamask-institutional/institutional-features": "^1.3.5", + "@metamask-institutional/custody-controller": "^0.3.0", + "@metamask-institutional/custody-keyring": "^2.1.0", + "@metamask-institutional/extension": "^0.3.28", + "@metamask-institutional/institutional-features": "^1.3.6", "@metamask-institutional/portfolio-dashboard": "^1.4.1", "@metamask-institutional/rpc-allowlist": "^1.0.3", - "@metamask-institutional/sdk": "^0.1.30", - "@metamask-institutional/transaction-update": "^0.2.5", - "@metamask-institutional/types": "^1.1.0", + "@metamask-institutional/sdk": "^0.2.0", + "@metamask-institutional/transaction-update": "^0.2.6", + "@metamask-institutional/types": "^1.2.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.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", - "@metamask/bitcoin-wallet-snap": "^0.6.0", + "@metamask/bitcoin-wallet-snap": "^0.8.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", @@ -317,21 +314,22 @@ "@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.4.0", "@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/json-rpc-engine": "^10.0.0", + "@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", - "@metamask/message-signing-snap": "^0.3.3", + "@metamask/message-signing-snap": "^0.4.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/name-controller": "^8.0.0", "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", @@ -344,25 +342,25 @@ "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.35.1", - "@metamask/preinstalled-example-snap": "^0.1.0", + "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", - "@metamask/rpc-errors": "^6.2.1", + "@metamask/rpc-errors": "^7.0.0", "@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/selected-network-controller": "^18.0.2", + "@metamask/signature-controller": "^20.0.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": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch", - "@metamask/transaction-controller": "^37.0.0", + "@metamask/snaps-controllers": "^9.11.1", + "@metamask/snaps-execution-environments": "^6.9.1", + "@metamask/snaps-rpc-methods": "^11.5.0", + "@metamask/snaps-sdk": "^6.9.0", + "@metamask/snaps-utils": "^8.4.1", + "@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", @@ -372,13 +370,14 @@ "@sentry/types": "^8.33.1", "@sentry/utils": "^8.33.1", "@swc/core": "1.4.11", - "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", + "@trezor/connect-web": "^9.4.0", "@zxing/browser": "^0.1.4", "@zxing/library": "0.20.0", "await-semaphore": "^0.1.1", "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", @@ -389,25 +388,26 @@ "currency-formatter": "^1.4.2", "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", + "eth-chainlist": "~0.0.498", "eth-ens-namehash": "^2.0.8", "eth-lattice-keyring": "^0.12.4", "eth-method-registry": "^4.0.0", - "eth-rpc-errors": "^4.0.2", "ethereumjs-util": "^7.0.10", "extension-port-stream": "^3.0.0", "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", "jest-junit": "^14.0.1", - "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^5.0.1", "labeled-stream-splicer": "^2.0.2", "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", @@ -427,7 +427,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", @@ -459,6 +460,7 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", "@babel/register": "^7.22.15", + "@jest/globals": "^29.7.0", "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/lavadome-core": "0.0.10", "@lavamoat/lavapack": "^6.1.0", @@ -475,8 +477,9 @@ "@metamask/eslint-plugin-design-tokens": "^1.1.0", "@metamask/forwarder": "^1.1.0", "@metamask/phishing-warning": "^4.0.0", + "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", - "@metamask/test-dapp": "^8.4.0", + "@metamask/test-dapp": "8.7.0", "@octokit/core": "^3.6.0", "@open-rpc/meta-schema": "^1.14.6", "@open-rpc/mock-server": "^1.7.5", @@ -503,6 +506,7 @@ "@storybook/test-runner": "^0.14.1", "@storybook/theming": "^7.6.20", "@swc/helpers": "^0.5.7", + "@testing-library/dom": "^7.31.2", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^10.4.8", "@testing-library/react-hooks": "^8.0.1", @@ -604,7 +608,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..41b04a9b5210 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -1,4 +1,6 @@ [ + "*.btc*.quiknode.pro", + "accounts.api.cx.metamask.io", "acl.execution.metamask.io", "api.blockchair.com", "api.lens.dev", @@ -6,6 +8,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 +16,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 +28,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", @@ -49,15 +57,10 @@ "test.metamask-phishing.io", "token.api.cx.metamask.io", "tokens.api.cx.metamask.io", + "transaction.api.cx.metamask.io", "tx-sentinel-ethereum-mainnet.api.cx.metamask.io", - "unresponsive-rpc.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/hardware-wallets.ts b/shared/constants/hardware-wallets.ts index 96e50ed7c17e..6fdfbedd9c04 100644 --- a/shared/constants/hardware-wallets.ts +++ b/shared/constants/hardware-wallets.ts @@ -18,6 +18,7 @@ export enum HardwareKeyringNames { export enum HardwareDeviceNames { ledger = 'ledger', trezor = 'trezor', + oneKeyViaTrezor = 'OneKey via Trezor', lattice = 'lattice', qr = 'QR Hardware', } diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 9994599e73cf..8107a1040127 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,10 +557,15 @@ 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', NavPermissionsOpened = 'Permissions Opened', + UpdatePermissionedNetworks = 'Update Permissioned Networks', + UpdatePermissionedAccounts = 'Update Permissioned Accounts', + ViewPermissionedNetworks = 'View Permissioned Networks', + ViewPermissionedAccounts = 'View Permissioned Accounts', NavNetworkMenuOpened = 'Network Menu Opened', NavSettingsOpened = 'Settings Opened', NavAccountSwitched = 'Account Switched', @@ -580,6 +590,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 +631,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 +639,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', @@ -729,7 +742,8 @@ export enum MetaMetricsEventName { sendFlowExited = 'Send Flow Exited', sendRecipientSelected = 'Send Recipient Selected', sendSwapQuoteError = 'Send Swap Quote Error', - sendSwapQuoteFetched = 'Send Swap Quote Fetched', + sendSwapQuoteRequested = 'Send Swap Quote Requested', + sendSwapQuoteReceived = 'Send Swap Quote Received', sendTokenModalOpened = 'Send Token Modal Opened', } @@ -773,12 +787,15 @@ export enum MetaMetricsEventCategory { NotificationsActivationFlow = 'Notifications Activation Flow', NotificationSettings = 'Notification Settings', Petnames = 'Petnames', + // eslint-disable-next-line @typescript-eslint/no-shadow + Permissions = 'Permissions', Phishing = 'Phishing', ProfileSyncing = 'Profile Syncing', PushNotifications = 'Notifications', 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..67be9f72cee6 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -6,13 +6,13 @@ import { SignatureController } from '@metamask/signature-controller'; import { NetworkController } from '@metamask/network-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import PreferencesController from '../../app/scripts/controllers/preferences-controller'; +import { PreferencesController } from '../../app/scripts/controllers/preferences-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { AppStateController } from '../../app/scripts/controllers/app-state'; +import { AppStateController } from '../../app/scripts/controllers/app-state-controller'; // 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/network.ts b/shared/constants/network.ts index a98417794d81..e911ce1aabf5 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -96,6 +96,7 @@ export const NETWORK_NAMES = { HOMESTEAD: 'homestead', }; +export const CHAIN_SPEC_URL = 'https://chainid.network/chains.json'; /** * An object containing all of the chain ids for networks both built in and * those that we have added custom code to support our feature set. @@ -145,6 +146,7 @@ export const CHAIN_IDS = { CHZ: '0x15b38', NUMBERS: '0x290b', SEI: '0x531', + APE_TESTNET: '0x8157', BERACHAIN: '0x138d5', METACHAIN_ONE: '0x1b6e6', ARBITRUM_SEPOLIA: '0x66eee', @@ -447,6 +449,7 @@ export const NUMBERS_MAINNET_IMAGE_URL = './images/numbers-mainnet.svg'; export const NUMBERS_TOKEN_IMAGE_URL = './images/numbers-token.png'; export const SEI_IMAGE_URL = './images/sei.svg'; export const NEAR_IMAGE_URL = './images/near.svg'; +export const APE_TESTNET_IMAGE_URL = './images/ape.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -779,6 +782,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.ZKATANA]: ZKATANA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ZORA_MAINNET]: ZORA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.FILECOIN]: FILECOIN_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.APE_TESTNET]: APE_TESTNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.BASE]: BASE_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.NUMBERS]: NUMBERS_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.SEI]: SEI_IMAGE_URL, 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..c068e4e8e35c --- /dev/null +++ b/shared/lib/accounts/bitcoin-wallet-snap.ts @@ -0,0 +1,10 @@ +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 = + BitcoinWalletSnap.snapId as SnapId; + +export const BITCOIN_WALLET_NAME: string = + BitcoinWalletSnap.manifest.proposedName; diff --git a/shared/lib/fetch-with-cache.ts b/shared/lib/fetch-with-cache.ts index 969fba9f869f..66610ec925b8 100644 --- a/shared/lib/fetch-with-cache.ts +++ b/shared/lib/fetch-with-cache.ts @@ -7,6 +7,7 @@ const fetchWithCache = async ({ fetchOptions = {}, cacheOptions: { cacheRefreshTime = MINUTE * 6, timeout = SECOND * 30 } = {}, functionName = '', + allowStale = false, }: { url: string; // TODO: Replace `any` with type @@ -16,6 +17,7 @@ const fetchWithCache = async ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any cacheOptions?: Record; functionName: string; + allowStale?: boolean; }) => { if ( fetchOptions.body || @@ -49,6 +51,11 @@ const fetchWithCache = async ({ ...fetchOptions, }); if (!response.ok) { + const message = `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`; + if (allowStale) { + console.debug(`${message}. Returning cached result`); + return cachedResponse; + } throw new Error( `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`, ); diff --git a/shared/lib/four-byte.test.ts b/shared/lib/four-byte.test.ts index 2867aa2e51b7..77271c4aeba3 100644 --- a/shared/lib/four-byte.test.ts +++ b/shared/lib/four-byte.test.ts @@ -10,12 +10,14 @@ import { getMethodDataAsync, getMethodFrom4Byte } from './four-byte'; const FOUR_BYTE_MOCK = TRANSACTION_DATA_FOUR_BYTE.slice(0, 10); describe('Four Byte', () => { - const fetchMock = jest.fn(); - describe('getMethodFrom4Byte', () => { - it('returns signature with earliest creation date', async () => { + const fetchMock = jest.fn(); + + beforeEach(() => { jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + }); + it('returns signature with earliest creation date', async () => { fetchMock.mockResolvedValue({ ok: true, json: async () => FOUR_BYTE_RESPONSE, @@ -44,6 +46,25 @@ describe('Four Byte', () => { expect(await getMethodFrom4Byte(prefix)).toBeUndefined(); }, ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + ['undefined', { results: undefined }], + ['object', { results: {} }], + ['empty', { results: [] }], + ])( + 'returns `undefined` if fourByteResponse.results is %s', + async (_: string, mockResponse: { results: unknown }) => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await getMethodFrom4Byte('0x913aa952'); + + expect(result).toBeUndefined(); + }, + ); }); describe('getMethodDataAsync', () => { diff --git a/shared/lib/four-byte.ts b/shared/lib/four-byte.ts index e28f4d4c0c5c..c6b9da22e617 100644 --- a/shared/lib/four-byte.ts +++ b/shared/lib/four-byte.ts @@ -34,6 +34,10 @@ export async function getMethodFrom4Byte( functionName: 'getMethodFrom4Byte', })) as FourByteResponse; + if (!fourByteResponse.results?.length) { + return undefined; + } + fourByteResponse.results.sort((a, b) => { return new Date(a.created_at).getTime() < new Date(b.created_at).getTime() ? -1 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..1dd50b736222 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -9,6 +9,7 @@ import { log as sentryLogger } from '../../app/scripts/lib/setupSentry'; * The supported trace names. */ export enum TraceName { + AccountList = 'Account List', BackgroundConnect = 'Background Connect', DeveloperTest = 'Developer Test', FirstRender = 'First Render', @@ -21,6 +22,7 @@ export enum TraceName { NotificationDisplay = 'Notification Display', PPOMValidation = 'PPOM Validation', SetupStore = 'Setup Store', + Signature = 'Signature', Transaction = 'Transaction', UIStartup = 'UI Startup', } @@ -31,6 +33,11 @@ const ID_DEFAULT = 'default'; const OP_DEFAULT = 'custom'; const tracesByKey: Map = new Map(); +const durationsByName: { [name: string]: number } = {}; + +if (process.env.IN_TEST && globalThis.stateHooks) { + globalThis.stateHooks.getCustomTraces = () => durationsByName; +} type PendingTrace = { end: (timestamp?: number) => void; @@ -154,9 +161,8 @@ export function endTrace(request: EndTraceRequest) { const { request: pendingRequest, startTime } = pendingTrace; const endTime = timestamp ?? getPerformanceTimestamp(); - const duration = endTime - startTime; - log('Finished trace', name, id, duration, { request: pendingRequest }); + logTrace(pendingRequest, startTime, endTime); } function traceCallback(request: TraceRequest, fn: TraceCallback): T { @@ -180,9 +186,7 @@ function traceCallback(request: TraceRequest, fn: TraceCallback): T { }, () => { const end = Date.now(); - const duration = end - start; - - log('Finished trace', name, duration, { error, request }); + logTrace(request, start, end, error); }, ) as T; }; @@ -241,6 +245,22 @@ function startSpan( }); } +function logTrace( + request: TraceRequest, + startTime: number, + endTime: number, + error?: unknown, +) { + const duration = endTime - startTime; + const { name } = request; + + if (process.env.IN_TEST) { + durationsByName[name] = duration; + } + + log('Finished trace', name, duration, { request, error }); +} + function getTraceId(request: TraceRequest) { return request.id ?? ID_DEFAULT; } diff --git a/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/error.test.ts b/shared/modules/error.test.ts index 247ef302d09e..7fab2ee4e2e4 100644 --- a/shared/modules/error.test.ts +++ b/shared/modules/error.test.ts @@ -24,7 +24,7 @@ describe('error module', () => { expect(log.error).toHaveBeenCalledWith('test'); }); - it('calls loglevel.error with the parameter passed in when parameter is not an instance of Error', () => { + it('calls loglevel.error with string representation of parameter passed in when parameter is not an instance of Error', () => { logErrorWithMessage({ test: 'test' }); expect(log.error).toHaveBeenCalledWith({ test: 'test' }); }); diff --git a/shared/modules/error.ts b/shared/modules/error.ts index fa212365570f..04b754625257 100644 --- a/shared/modules/error.ts +++ b/shared/modules/error.ts @@ -1,24 +1,33 @@ import log from 'loglevel'; +import { + getErrorMessage as _getErrorMessage, + hasProperty, + isObject, + isErrorWithMessage, +} from '@metamask/utils'; + +export { isErrorWithMessage } from '@metamask/utils'; /** - * Type guard for determining whether the given value is an error object with a - * `message` property, such as an instance of Error. - * - * TODO: Remove once this becomes available at @metamask/utils + * Attempts to obtain the message from a possible error object, defaulting to an + * empty string if it is impossible to do so. * - * @param error - The object to check. - * @returns True or false, depending on the result. + * @param error - The possible error to get the message from. + * @returns The message if `error` is an object with a `message` property; + * the string version of `error` if it is not `undefined` or `null`; otherwise + * an empty string. */ -export function isErrorWithMessage( - error: unknown, -): error is { message: string } { - return typeof error === 'object' && error !== null && 'message' in error; +// TODO: Remove completely once changes implemented in @metamask/utils +export function getErrorMessage(error: unknown): string { + return isErrorWithMessage(error) && + hasProperty(error, 'cause') && + isObject(error.cause) && + hasProperty(error.cause, 'message') && + typeof error.cause.message === 'string' + ? error.cause.message + : _getErrorMessage(error); } export function logErrorWithMessage(error: unknown) { - if (isErrorWithMessage(error)) { - log.error(error.message); - } else { - log.error(error); - } + log.error(isErrorWithMessage(error) ? getErrorMessage(error) : error); } diff --git a/shared/modules/hexstring-utils.test.js b/shared/modules/hexstring-utils.test.js index 9bfeac1978e1..5fe876428bcc 100644 --- a/shared/modules/hexstring-utils.test.js +++ b/shared/modules/hexstring-utils.test.js @@ -1,5 +1,5 @@ import { toChecksumAddress } from 'ethereumjs-util'; -import { isValidHexAddress, isPossibleAddress } from './hexstring-utils'; +import { isPossibleAddress, isValidHexAddress } from './hexstring-utils'; describe('hexstring utils', function () { describe('isPossibleAddress', function () { 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/shared/modules/mv3.utils.js b/shared/modules/mv3.utils.js index 417484c46de6..24a4bc8880f4 100644 --- a/shared/modules/mv3.utils.js +++ b/shared/modules/mv3.utils.js @@ -6,14 +6,13 @@ const runtimeManifest = /** * A boolean indicating whether the manifest of the current extension is set to manifest version 3. * - * We have found that when this is run early in a service worker process, the runtime manifest is - * unavailable. That's why we have a fallback using the ENABLE_MV3 constant. The fallback is also - * used in unit tests. + * If this function is running in the Extension, it will use the runtime manifest. + * If this function is running in Node doing a build job, it will read process.env.ENABLE_MV3. + * If this function is running in Node doing an E2E test, it will `fs.readFileSync` the manifest.json file. */ const isManifestV3 = runtimeManifest ? runtimeManifest.manifest_version === 3 - : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it will - // always be a string + : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it can be a string process.env.ENABLE_MV3 === true || process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined; diff --git a/shared/modules/selectors/index.test.ts b/shared/modules/selectors/index.test.ts index f1dc4fee5ec2..9f0b1b201a5c 100644 --- a/shared/modules/selectors/index.test.ts +++ b/shared/modules/selectors/index.test.ts @@ -1,12 +1,16 @@ +// Mocha type definitions are conflicting with Jest +import { it as jestIt } from '@jest/globals'; + import { createSwapsMockStore } from '../../../test/jest'; import { CHAIN_IDS } from '../../constants/network'; import { mockNetworkState } from '../../../test/stub/networks'; import { - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, getCurrentChainSupportsSmartTransactions, getSmartTransactionsEnabled, getIsSmartTransaction, getIsSmartTransactionsOptInModalAvailable, + getSmartTransactionsPreferenceEnabled, } from '.'; describe('Selectors', () => { @@ -65,115 +69,190 @@ describe('Selectors', () => { }; }; - describe('getSmartTransactionsOptInStatus', () => { - it('should return the smart transactions opt-in status', () => { - const state = createMockState(); - const result = getSmartTransactionsOptInStatus(state); - expect(result).toBe(true); - }); - }); + describe('getSmartTransactionsOptInStatusForMetrics and getSmartTransactionsPreferenceEnabled', () => { + const createMockOptInStatusState = (status: boolean | null) => { + return { + metamask: { + preferences: { + smartTransactionsOptInStatus: status, + }, + }, + }; + }; + describe('getSmartTransactionsOptInStatusForMetrics', () => { + jestIt('should return the smart transactions opt-in status', () => { + const state = createMockState(); + const result = getSmartTransactionsOptInStatusForMetrics(state); + expect(result).toBe(true); + }); - describe('getCurrentChainSupportsSmartTransactions', () => { - it('should return true if the chain ID is allowed for smart transactions', () => { - const state = createMockState(); - const result = getCurrentChainSupportsSmartTransactions(state); - expect(result).toBe(true); + jestIt.each([ + { status: true, expected: true }, + { status: false, expected: false }, + { status: null, expected: null }, + ])( + 'should return $expected if the smart transactions opt-in status is $status', + ({ status, expected }) => { + const state = createMockOptInStatusState(status); + const result = getSmartTransactionsOptInStatusForMetrics(state); + expect(result).toBe(expected); + }, + ); }); - it('should return false if the chain ID is not allowed for smart transactions', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + describe('getSmartTransactionsPreferenceEnabled', () => { + jestIt( + 'should return the smart transactions preference enabled status', + () => { + const state = createMockState(); + const result = getSmartTransactionsPreferenceEnabled(state); + expect(result).toBe(true); }, - }; - const result = getCurrentChainSupportsSmartTransactions(newState); - expect(result).toBe(false); + ); + + jestIt.each([ + { status: true, expected: true }, + { status: false, expected: false }, + { status: null, expected: true }, + ])( + 'should return $expected if the smart transactions opt-in status is $status', + ({ status, expected }) => { + const state = createMockOptInStatusState(status); + const result = getSmartTransactionsPreferenceEnabled(state); + expect(result).toBe(expected); + }, + ); }); }); + describe('getCurrentChainSupportsSmartTransactions', () => { + jestIt( + 'should return true if the chain ID is allowed for smart transactions', + () => { + const state = createMockState(); + const result = getCurrentChainSupportsSmartTransactions(state); + expect(result).toBe(true); + }, + ); + + jestIt( + 'should return false if the chain ID is not allowed for smart transactions', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + }, + }; + const result = getCurrentChainSupportsSmartTransactions(newState); + expect(result).toBe(false); + }, + ); + }); + describe('getSmartTransactionsEnabled', () => { - it('returns true if feature flag is enabled, not a HW and is Ethereum network', () => { - const state = createSwapsMockStore(); - expect(getSmartTransactionsEnabled(state)).toBe(true); - }); + jestIt( + 'returns true if feature flag is enabled, not a HW and is Ethereum network', + () => { + const state = createSwapsMockStore(); + expect(getSmartTransactionsEnabled(state)).toBe(true); + }, + ); - it('returns false if feature flag is disabled, not a HW and is Ethereum network', () => { - const state = createSwapsMockStore(); - state.metamask.swapsState.swapsFeatureFlags.smartTransactions.extensionActive = - false; - expect(getSmartTransactionsEnabled(state)).toBe(false); - }); + jestIt( + 'returns false if feature flag is disabled, not a HW and is Ethereum network', + () => { + const state = createSwapsMockStore(); + state.metamask.swapsState.swapsFeatureFlags.smartTransactions.extensionActive = + false; + expect(getSmartTransactionsEnabled(state)).toBe(false); + }, + ); - it('returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network', () => { - const state = createSwapsMockStore(); - state.metamask.smartTransactionsState.liveness = false; - expect(getSmartTransactionsEnabled(state)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW, STX liveness is false and is Ethereum network', + () => { + const state = createSwapsMockStore(); + state.metamask.smartTransactionsState.liveness = false; + expect(getSmartTransactionsEnabled(state)).toBe(false); + }, + ); - it('returns true if feature flag is enabled, is a HW and is Ethereum network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - internalAccounts: { - ...state.metamask.internalAccounts, - selectedAccount: 'account2', - accounts: { - account2: { - metadata: { - keyring: { - type: 'Trezor Hardware', + jestIt( + 'returns true if feature flag is enabled, is a HW and is Ethereum network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + internalAccounts: { + ...state.metamask.internalAccounts, + selectedAccount: 'account2', + accounts: { + account2: { + metadata: { + keyring: { + type: 'Trezor Hardware', + }, }, }, }, }, }, - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(true); - }); + }; + expect(getSmartTransactionsEnabled(newState)).toBe(true); + }, + ); - it('returns false if feature flag is enabled, not a HW and is Polygon network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW and is Polygon network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + }, + }; + expect(getSmartTransactionsEnabled(newState)).toBe(false); + }, + ); - it('returns false if feature flag is enabled, not a HW and is BSC network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.BSC }), - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW and is BSC network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.BSC }), + }, + }; + expect(getSmartTransactionsEnabled(newState)).toBe(false); + }, + ); - it('returns false if feature flag is enabled, not a HW and is Linea network', () => { - const state = createSwapsMockStore(); - const newState = { - ...state, - metamask: { - ...state.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }), - }, - }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); - }); + jestIt( + 'returns false if feature flag is enabled, not a HW and is Linea network', + () => { + const state = createSwapsMockStore(); + const newState = { + ...state, + metamask: { + ...state.metamask, + ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }), + }, + }; + expect(getSmartTransactionsEnabled(newState)).toBe(false); + }, + ); - it('returns false if a snap account is used', () => { + jestIt('returns false if a snap account is used', () => { const state = createSwapsMockStore(); state.metamask.internalAccounts.selectedAccount = '36eb02e0-7925-47f0-859f-076608f09b69'; @@ -182,13 +261,16 @@ describe('Selectors', () => { }); describe('getIsSmartTransaction', () => { - it('should return true if smart transactions are opt-in and enabled', () => { - const state = createMockState(); - const result = getIsSmartTransaction(state); - expect(result).toBe(true); - }); + jestIt( + 'should return true if smart transactions are opt-in and enabled', + () => { + const state = createMockState(); + const result = getIsSmartTransaction(state); + expect(result).toBe(true); + }, + ); - it('should return false if smart transactions are not opt-in', () => { + jestIt('should return false if smart transactions are not opt-in', () => { const state = createMockState(); const newState = { ...state, @@ -204,7 +286,7 @@ describe('Selectors', () => { expect(result).toBe(false); }); - it('should return false if smart transactions are not enabled', () => { + jestIt('should return false if smart transactions are not enabled', () => { const state = createMockState(); const newState = { ...state, @@ -236,103 +318,121 @@ describe('Selectors', () => { }); describe('getIsSmartTransactionsOptInModalAvailable', () => { - it('returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, + jestIt( + 'returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true); + }, + ); - it('returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, + jestIt( + 'returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), }, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, + jestIt( + 'returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + ...mockNetworkState({ + chainId: CHAIN_IDS.MAINNET, + rpcUrl: 'https://mainnet.quiknode.pro/', + }), }, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - rpcUrl: 'https://mainnet.quiknode.pro/', - }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance', () => { - const state = createMockState(); - expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false); - }); + jestIt( + 'returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance', + () => { + const state = createMockState(); + expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - accounts: { - ...state.metamask.accounts, - '0x123': { - address: '0x123', - balance: '0x0', + jestIt( + 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + accounts: { + ...state.metamask.accounts, + '0x123': { + address: '0x123', + balance: '0x0', + }, }, }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); - it('returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)', () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - accounts: { - ...state.metamask.accounts, - '0x123': { - address: '0x123', - balance: '0x00', + jestIt( + 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)', + () => { + const state = createMockState(); + const newState = { + ...state, + metamask: { + ...state.metamask, + preferences: { + ...state.metamask.preferences, + smartTransactionsOptInStatus: null, + }, + accounts: { + ...state.metamask.accounts, + '0x123': { + address: '0x123', + balance: '0x00', + }, }, }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }); + }; + expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); + }, + ); }); }); diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index 1c3147632381..a02fe63692b3 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -1,3 +1,4 @@ +import { createSelector } from 'reselect'; import { getAllowedSmartTransactionsChainIds, SKIP_STX_RPC_URL_CHECK_CHAIN_IDS, @@ -7,6 +8,7 @@ import { getCurrentNetwork, accountSupportsSmartTx, getSelectedAccount, + getPreferences, // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../ui/selectors/selectors'; // TODO: Migrate shared selectors to this file. @@ -56,11 +58,60 @@ type SmartTransactionsMetaMaskState = { }; }; -export const getSmartTransactionsOptInStatus = ( - state: SmartTransactionsMetaMaskState, -): boolean | null => { - return state.metamask.preferences?.smartTransactionsOptInStatus ?? null; -}; +/** + * Returns the user's explicit opt-in status for the smart transactions feature. + * This should only be used for reading the user's internal opt-in status, and + * not for determining if the smart transactions user preference is enabled. + * + * To determine if the smart transactions user preference is enabled, use + * getSmartTransactionsPreferenceEnabled instead. + * + * @param state - The state object. + * @returns true if the user has explicitly opted in, false if they have opted out, + * or null if they have not explicitly opted in or out. + */ +export const getSmartTransactionsOptInStatusInternal = createSelector( + getPreferences, + (preferences: { + smartTransactionsOptInStatus?: boolean | null; + }): boolean | null => { + return preferences?.smartTransactionsOptInStatus ?? null; + }, +); + +/** + * Returns the user's explicit opt-in status for the smart transactions feature. + * This should only be used for metrics collection, and not for determining if the + * smart transactions user preference is enabled. + * + * To determine if the smart transactions user preference is enabled, use + * getSmartTransactionsPreferenceEnabled instead. + * + * @param state - The state object. + * @returns true if the user has explicitly opted in, false if they have opted out, + * or null if they have not explicitly opted in or out. + */ +export const getSmartTransactionsOptInStatusForMetrics = createSelector( + getSmartTransactionsOptInStatusInternal, + (optInStatus: boolean | null): boolean | null => optInStatus, +); + +/** + * Returns the user's preference for the smart transactions feature. + * Defaults to `true` if the user has not set a preference. + * + * @param state + * @returns + */ +export const getSmartTransactionsPreferenceEnabled = createSelector( + getSmartTransactionsOptInStatusInternal, + (optInStatus: boolean | null): boolean => { + // In the absence of an explicit opt-in or opt-out, + // the Smart Transactions toggle is enabled. + const DEFAULT_SMART_TRANSACTIONS_ENABLED = true; + return optInStatus ?? DEFAULT_SMART_TRANSACTIONS_ENABLED; + }, +); export const getCurrentChainSupportsSmartTransactions = ( state: SmartTransactionsMetaMaskState, @@ -105,7 +156,7 @@ export const getIsSmartTransactionsOptInModalAvailable = ( return ( getCurrentChainSupportsSmartTransactions(state) && getIsAllowedRpcUrlForSmartTransactions(state) && - getSmartTransactionsOptInStatus(state) === null && + getSmartTransactionsOptInStatusInternal(state) === null && hasNonZeroBalance(state) ); }; @@ -132,7 +183,10 @@ export const getSmartTransactionsEnabled = ( export const getIsSmartTransaction = ( state: SmartTransactionsMetaMaskState, ): boolean => { - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); + const smartTransactionsPreferenceEnabled = + getSmartTransactionsPreferenceEnabled(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state); - return Boolean(smartTransactionsOptInStatus && smartTransactionsEnabled); + return Boolean( + smartTransactionsPreferenceEnabled && smartTransactionsEnabled, + ); }; diff --git a/sonar-project.properties b/sonar-project.properties index a965dab30d7e..4362539a94ff 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,9 +1,15 @@ +# Note: Updating this file on feature branches or forks will not reflect changes in the SonarCloud scan results. +# The SonarCloud scan workflow always uses the latest version from the default branch. +# This means any changes made to this file in a feature branch will not be considered until they are merged. + sonar.projectKey=metamask-extension 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/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index f02705a2540b..7be24a1389c6 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -183,6 +183,21 @@ export const permitSignatureMsg = { }, } as SignatureRequestType; +export const permitNFTSignatureMsg = { + id: 'c5067710-87cf-11ef-916c-71f266571322', + status: 'unapproved', + time: 1728651190529, + type: 'eth_signTypedData', + msgParams: { + data: '{"domain":{"name":"Uniswap V3 Positions NFT-V1","version":"1","chainId":1,"verifyingContract":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88"},"types":{"Permit":[{"name":"spender","type":"address"},{"name":"tokenId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","message":{"spender":"0x00000000Ede6d8D217c60f93191C060747324bca","tokenId":"3606393","nonce":"0","deadline":"1734995006"}}', + from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + signatureMethod: 'eth_signTypedData_v4', + requestId: 2874791875, + origin: 'https://metamask.github.io', + }, +} as SignatureRequestType; + export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', securityAlertResponse: { diff --git a/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 deleted file mode 100644 index 60e0ea378b75..000000000000 --- a/test/e2e/accounts/common.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { privateToAddress } from 'ethereumjs-util'; -import messages from '../../../app/_locales/en/messages.json'; -import FixtureBuilder from '../fixture-builder'; -import { - PRIVATE_KEY, - PRIVATE_KEY_TWO, - WINDOW_TITLES, - clickSignOnSignatureConfirmation, - switchToOrOpenDapp, - unlockWallet, - validateContractDetails, - multipleGanacheOptions, - regularDelayMs, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; -import { retry } from '../../../development/lib/retry'; - -/** - * These are fixtures specific to Account Snap E2E tests: - * -- connected to Test Dapp - * -- two private keys with 25 ETH each - * - * @param title - */ -export const accountSnapFixtures = (title: string | undefined) => { - return { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp({ - restrictReturnedAccounts: false, - }) - .build(), - ganacheOptions: multipleGanacheOptions, - title, - }; -}; - -// convert PRIVATE_KEY to public key -export const PUBLIC_KEY = privateToAddress( - Buffer.from(PRIVATE_KEY.slice(2), 'hex'), -).toString('hex'); - -export async function installSnapSimpleKeyring( - driver: Driver, - isAsyncFlow: boolean, -) { - await unlockWallet(driver); - - // navigate to test Snaps page and connect - await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); - - await driver.clickElement('#connectButton'); - - await driver.delay(500); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.delay(500); - - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - - await driver.findElement({ text: 'Add to MetaMask', tag: 'h3' }); - - await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); - - await driver.waitForSelector({ text: 'Confirm' }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - - await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ - text: 'OK', - tag: 'button', - }); - - // Wait until popup is closed before proceeding - await driver.waitUntilXWindowHandles(2); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await driver.waitForSelector({ - text: 'Connected', - tag: 'span', - }); - - if (isAsyncFlow) { - await toggleAsyncFlow(driver); - } -} - -async function toggleAsyncFlow(driver: Driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await driver.clickElement('[data-testid="use-sync-flow-toggle"]'); -} - -export async function importKeyAndSwitch(driver: Driver) { - await driver.clickElement({ - text: 'Import account', - tag: 'div', - }); - - await driver.fill('#import-account-private-key', PRIVATE_KEY_TWO); - - await driver.clickElement({ - text: 'Import Account', - tag: 'button', - }); - - // Click "Create" on the Snap's confirmation popup - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Create', - }); - // Click the add account button on the naming modal - await driver.clickElement({ - css: '[data-testid="submit-add-account-with-name"]', - text: 'Add account', - }); - // Click the ok button on the success modal - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Ok', - }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await switchToAccount2(driver); -} - -export async function makeNewAccountAndSwitch(driver: Driver) { - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - // Click "Create" on the Snap's confirmation popup - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Create', - }); - // Click the add account button on the naming modal - await driver.clickElement({ - css: '[data-testid="submit-add-account-with-name"]', - text: 'Add account', - }); - // Click the ok button on the success modal - await driver.clickElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Ok', - }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - const newPublicKey = await ( - await driver.findElement({ - text: '0x', - tag: 'p', - }) - ).getText(); - - await switchToAccount2(driver); - - return newPublicKey; -} - -async function switchToAccount2(driver: Driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); - - // click on Accounts - await driver.clickElement('[data-testid="account-menu-icon"]'); - - await driver.clickElement({ - tag: 'Button', - text: 'SSK Account', - }); - - await driver.assertElementNotPresent({ - tag: 'header', - text: 'Select an account', - }); -} - -export async function connectAccountToTestDapp(driver: Driver) { - await switchToOrOpenDapp(driver); - - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); -} - -export async function disconnectFromTestDapp(driver: Driver) { - 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="account-list-item-menu-button"]'); - await driver.clickElement({ text: 'Disconnect', tag: 'button' }); - await driver.clickElement('[data-testid ="disconnect-all"]'); -} - -export async function approveOrRejectRequest(driver: Driver, flowType: string) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); - - await driver.clickElementUsingMouseMove({ - text: 'List requests', - tag: 'div', - }); - - await driver.clickElement({ - text: 'List Requests', - tag: 'button', - }); - - // get the JSON from the screen - const requestJSON = await ( - await driver.findElement({ - text: '"scope":', - tag: 'div', - }) - ).getText(); - - const requestID = JSON.parse(requestJSON)[0].id; - - if (flowType === 'approve') { - await driver.clickElementUsingMouseMove({ - text: 'Approve request', - tag: 'div', - }); - - await driver.fill('#approve-request-request-id', requestID); - - await driver.clickElement({ - text: 'Approve Request', - tag: 'button', - }); - } else if (flowType === 'reject') { - await driver.clickElementUsingMouseMove({ - text: 'Reject request', - tag: 'div', - }); - - await driver.fill('#reject-request-request-id', requestID); - - await driver.clickElement({ - text: 'Reject Request', - tag: 'button', - }); - } - - // Close the SnapSimpleKeyringDapp, so that 6 of the same tab doesn't pile up - await driver.closeWindow(); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); -} - -export async function signData( - driver: Driver, - locatorID: string, - newPublicKey: string, - flowType: string, -) { - const isAsyncFlow = flowType !== 'sync'; - - // This step can frequently fail, so retry it - await retry( - { - retries: 3, - delay: 2000, - }, - async () => { - await switchToOrOpenDapp(driver); - - await driver.clickElement(locatorID); - - // take extra time to load the popup - await driver.delay(500); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - }, - ); - - // these three don't have a contract details page - if (!['#ethSign', '#personalSign', '#signTypedData'].includes(locatorID)) { - await validateContractDetails(driver); - } - - await clickSignOnSignatureConfirmation({ driver }); - - if (isAsyncFlow) { - await driver.delay(2000); - - // This step can frequently fail, so retry it - await retry( - { - retries: 3, - delay: 1000, - }, - async () => { - // Navigate to the Notification window and click 'Go to site' button - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Go to site', - tag: 'button', - }); - }, - ); - - await driver.delay(1000); - await approveOrRejectRequest(driver, flowType); - } - - await driver.delay(500); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - if (flowType === 'sync' || flowType === 'approve') { - if (locatorID === '#ethSign') { - // there is no Verify button for #ethSign - await driver.findElement({ - css: '#ethSignResult', - text: '0x', // we are just making sure that it contains ANY hex value - }); - } else { - await driver.clickElement(`${locatorID}Verify`); - - const resultLocator = - locatorID === '#personalSign' - ? '#personalSignVerifyECRecoverResult' // the verify span IDs are different with Personal Sign - : `${locatorID}VerifyResult`; - - await driver.findElement({ - css: resultLocator, - text: newPublicKey.toLowerCase(), - }); - } - } else if (flowType === 'reject') { - // ensure the transaction was rejected by the Snap - await driver.findElement({ - text: 'Error: Request rejected by user or snap.', - }); - } -} - -export async function createBtcAccount(driver: Driver) { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement({ - text: messages.addNewBitcoinAccount.message, - tag: 'button', - }); - await driver.clickElementAndWaitToDisappear( - { - text: 'Add account', - tag: 'button', - }, - // Longer timeout than usual, this reduces the flakiness - // around Bitcoin account creation (mainly required for - // Firefox) - 5000, - ); -} 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/accounts/snap-account-contract-interaction.spec.ts b/test/e2e/accounts/snap-account-contract-interaction.spec.ts deleted file mode 100644 index 4588d014802c..000000000000 --- a/test/e2e/accounts/snap-account-contract-interaction.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import GanacheContractAddressRegistry from '../seeder/ganache-contract-address-registry'; -import { scrollAndConfirmAndAssertConfirm } from '../tests/confirmations/helpers'; -import { - createDepositTransaction, - TestSuiteArguments, -} from '../tests/confirmations/transactions/shared'; -import { - multipleGanacheOptionsForType2Transactions, - withFixtures, - openDapp, - WINDOW_TITLES, - locateAccountBalanceDOM, - clickNestedButton, - ACCOUNT_2, -} from '../helpers'; -import FixtureBuilder from '../fixture-builder'; -import { SMART_CONTRACTS } from '../seeder/smart-contracts'; -import { installSnapSimpleKeyring, importKeyAndSwitch } from './common'; - -describe('Snap Account Contract interaction', function () { - const smartContract = SMART_CONTRACTS.PIGGYBANK; - - it('deposits to piggybank contract', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerSnapAccountConnectedToTestDapp() - .withPreferencesController({ - preferences: { - redesignedConfirmationsEnabled: true, - isRedesignedConfirmationsDeveloperEnabled: true, - }, - }) - .build(), - ganacheOptions: multipleGanacheOptionsForType2Transactions, - smartContract, - title: this.test?.fullTitle(), - }, - async ({ - driver, - contractRegistry, - ganacheServer, - }: TestSuiteArguments) => { - // Install Snap Simple Keyring and import key - await installSnapSimpleKeyring(driver, false); - await importKeyAndSwitch(driver); - - // Open DApp with contract - const contractAddress = await ( - contractRegistry as GanacheContractAddressRegistry - ).getContractAddress(smartContract); - await openDapp(driver, contractAddress); - - // Create and confirm deposit transaction - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await createDepositTransaction(driver); - await driver.waitUntilXWindowHandles(4); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.waitForSelector({ - css: 'h2', - text: 'Transaction request', - }); - await scrollAndConfirmAndAssertConfirm(driver); - - // Confirm the transaction activity - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await clickNestedButton(driver, 'Activity'); - await driver.waitForSelector( - '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', - ); - await driver.waitForSelector({ - css: '[data-testid="transaction-list-item-primary-currency"]', - text: '-4 ETH', - }); - - // renders the correct ETH balance - await locateAccountBalanceDOM(driver, ganacheServer, ACCOUNT_2); - }, - ); - }); -}); diff --git a/test/e2e/accounts/snap-account-eth-swap.spec.ts b/test/e2e/accounts/snap-account-eth-swap.spec.ts index 2a99287e5db1..2a0f230f0d54 100644 --- a/test/e2e/accounts/snap-account-eth-swap.spec.ts +++ b/test/e2e/accounts/snap-account-eth-swap.spec.ts @@ -1,6 +1,7 @@ import { withFixtures, defaultGanacheOptions, WINDOW_TITLES } from '../helpers'; import { Driver } from '../webdriver/driver'; import FixtureBuilder from '../fixture-builder'; +import { Ganache } from '../seeder/ganache'; import { buildQuote, reviewQuote, @@ -8,8 +9,9 @@ import { checkActivityTransaction, } from '../tests/swaps/shared'; import { TRADES_API_MOCK_RESULT } from '../../data/mock-data'; +import { installSnapSimpleKeyring } from '../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; import { Mockttp } from '../mock-e2e'; -import { installSnapSimpleKeyring } from './common'; const DAI = 'DAI'; const TEST_ETH = 'TESTETH'; @@ -34,8 +36,15 @@ describe('Snap Account - Swap', function () { title: this.test?.fullTitle(), testSpecificMock: mockSwapsTransactionQuote, }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); diff --git a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts deleted file mode 100644 index 24e996671da9..000000000000 --- a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Suite } from 'mocha'; -import FixtureBuilder from '../fixture-builder'; -import { - withFixtures, - multipleGanacheOptions, - tempToggleSettingRedesignedConfirmations, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - installSnapSimpleKeyring, - makeNewAccountAndSwitch, - connectAccountToTestDapp, - disconnectFromTestDapp, - signData, -} from './common'; - -describe('Snap Account Signatures and Disconnects', function (this: Suite) { - it('can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - ganacheOptions: multipleGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - const flowType = 'approve'; - const isAsyncFlow = true; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - - const newPublicKey = await makeNewAccountAndSwitch(driver); - - await tempToggleSettingRedesignedConfirmations(driver); - - // open the Test Dapp and connect Account 2 to it - await connectAccountToTestDapp(driver); - - // do #signTypedDataV3 - await signData(driver, '#signTypedDataV3', newPublicKey, flowType); - - // disconnect from the Test Dapp - await disconnectFromTestDapp(driver); - - // reconnect Account 2 to the Test Dapp - await connectAccountToTestDapp(driver); - - // do #signTypedDataV4 - await signData(driver, '#signTypedDataV4', newPublicKey, flowType); - }, - ); - }); -}); diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts deleted file mode 100644 index 536d8168b1a3..000000000000 --- a/test/e2e/accounts/snap-account-signatures.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Suite } from 'mocha'; -import { - tempToggleSettingRedesignedConfirmations, - withFixtures, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - installSnapSimpleKeyring, - makeNewAccountAndSwitch, - signData, -} from './common'; - -describe('Snap Account Signatures', function (this: Suite) { - this.timeout(120000); // This test is very long, so we need an unusually high timeout - - // Run sync, async approve, and async reject flows - // (in Jest we could do this with test.each, but that does not exist here) - ['sync', 'approve', 'reject'].forEach((flowType) => { - // generate title of the test from flowType - const title = `can sign with ${flowType} flow`; - - it(title, async () => { - await withFixtures( - accountSnapFixtures(title), - async ({ driver }: { driver: Driver }) => { - const isAsyncFlow = flowType !== 'sync'; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - - const newPublicKey = await makeNewAccountAndSwitch(driver); - - await tempToggleSettingRedesignedConfirmations(driver); - - // Run all 5 signature types - const locatorIDs = [ - '#personalSign', - '#signTypedData', - '#signTypedDataV3', - '#signTypedDataV4', - '#signPermit', - ]; - - for (const locatorID of locatorIDs) { - await signData(driver, locatorID, newPublicKey, flowType); - } - }, - ); - }); - }); -}); diff --git a/test/e2e/accounts/snap-account-transfers.spec.ts b/test/e2e/accounts/snap-account-transfers.spec.ts deleted file mode 100644 index cb344e188640..000000000000 --- a/test/e2e/accounts/snap-account-transfers.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Suite } from 'mocha'; -import { - sendTransaction, - withFixtures, - WINDOW_TITLES, - clickNestedButton, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - PUBLIC_KEY, - installSnapSimpleKeyring, - importKeyAndSwitch, - approveOrRejectRequest, -} from './common'; - -describe('Snap Account Transfers', function (this: Suite) { - it('can import a private key and transfer 1 ETH (sync flow)', async function () { - await withFixtures( - accountSnapFixtures(this.test?.fullTitle()), - async ({ driver }: { driver: Driver }) => { - await importPrivateKeyAndTransfer1ETH(driver, 'sync'); - }, - ); - }); - - it('can import a private key and transfer 1 ETH (async flow approve)', async function () { - await withFixtures( - accountSnapFixtures(this.test?.fullTitle()), - async ({ driver }: { driver: Driver }) => { - await importPrivateKeyAndTransfer1ETH(driver, 'approve'); - }, - ); - }); - - it('can import a private key and transfer 1 ETH (async flow reject)', async function () { - await withFixtures( - accountSnapFixtures(this.test?.fullTitle()), - async ({ driver }: { driver: Driver }) => { - await importPrivateKeyAndTransfer1ETH(driver, 'reject'); - }, - ); - }); - - /** - * @param driver - * @param flowType - */ - async function importPrivateKeyAndTransfer1ETH( - driver: Driver, - flowType: string, - ) { - const isAsyncFlow = flowType !== 'sync'; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - await importKeyAndSwitch(driver); - - // send 1 ETH from Account 2 to Account 1 - await sendTransaction(driver, PUBLIC_KEY, 1, isAsyncFlow); - - if (isAsyncFlow) { - await driver.assertElementNotPresent({ - text: 'Please complete the transaction on the Snap.', - }); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await driver.navigate(); - await driver.delay(2000); - await driver.clickElement({ - text: 'Go to site', - tag: 'button', - }); - - await driver.delay(1000); - await approveOrRejectRequest(driver, flowType); - } - - if (flowType === 'sync' || flowType === 'approve') { - // click on Accounts - await driver.clickElement('[data-testid="account-menu-icon"]'); - - // ensure one account has 26 ETH and the other has 24 ETH - await driver.findElement('[title="26 ETH"]'); - await driver.findElement('[title="24 ETH"]'); - } else if (flowType === 'reject') { - // ensure the transaction was rejected by the Snap - await clickNestedButton(driver, 'Activity'); - await driver.findElement( - '[data-original-title="Request rejected by user or snap."]', - ); - } - } -}); 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/benchmark.js b/test/e2e/benchmark.js index 738d766f8555..1f24a960d9eb 100755 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -17,6 +17,16 @@ const FixtureBuilder = require('./fixture-builder'); const DEFAULT_NUM_SAMPLES = 20; const ALL_PAGES = Object.values(PAGES); +const CUSTOM_TRACES = { + backgroundConnect: 'Background Connect', + firstReactRender: 'First Render', + getState: 'Get State', + initialActions: 'Initial Actions', + loadScripts: 'Load Scripts', + setupStore: 'Setup Store', + uiStartup: 'UI Startup', +}; + async function measurePage(pageName) { let metrics; await withFixtures( @@ -32,6 +42,7 @@ async function measurePage(pageName) { await driver.findElement('[data-testid="account-menu-icon"]'); await driver.navigate(pageName); await driver.delay(1000); + metrics = await driver.collectMetrics(); }, ); @@ -79,7 +90,7 @@ async function profilePageLoad(pages, numSamples, retries) { runResults.push(result); } - if (runResults.some((result) => result.navigation.lenth > 1)) { + if (runResults.some((result) => result.navigation.length > 1)) { throw new Error(`Multiple navigations not supported`); } else if ( runResults.some((result) => result.navigation[0].type !== 'navigate') @@ -107,6 +118,10 @@ async function profilePageLoad(pages, numSamples, retries) { ), }; + for (const [key, name] of Object.entries(CUSTOM_TRACES)) { + result[key] = runResults.map((metrics) => metrics[name]); + } + results[pageName] = { min: minResult(result), max: maxResult(result), diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index e7fef587f533..c3957cb6fbbf 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'; @@ -46,3 +46,6 @@ export const DAPP_ONE_URL = 'http://127.0.0.1:8081'; /* Default BTC address created using test SRP */ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; + +/* Default (mocked) BTC balance used by the Bitcoin RPC provider */ +export const DEFAULT_BTC_BALANCE = 1; // BTC diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 470e260ff959..95f35bf1694c 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -1,3 +1,6 @@ +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); const { FirstTimeFlowType } = require('../../shared/constants/onboarding'); @@ -127,6 +130,10 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { srcNetworkAllowlist: ['0x1', '0xa', '0xe708'], destNetworkAllowlist: ['0x1', '0xa', '0xe708'], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CurrencyController: { @@ -206,11 +213,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', @@ -222,6 +235,29 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, }, QueuedRequestController: { queuedRequestCount: 0, @@ -232,7 +268,9 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { SmartTransactionsController: { smartTransactionsState: { fees: {}, + feesByChainId: {}, liveness: true, + livenessByChainId: {}, smartTransactions: { [CHAIN_IDS.MAINNET]: [], }, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index cc30c261d22d..d73b959946c2 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -4,6 +4,9 @@ const { } = require('@metamask/snaps-utils'); const { merge, mergeWith } = require('lodash'); const { toHex } = require('@metamask/controller-utils'); +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); @@ -72,11 +75,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', @@ -88,6 +97,31 @@ function onboardingFixture() { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + showTestNetworks: false, + smartTransactionsOptInStatus: false, }, QueuedRequestController: { queuedRequestCount: 0, @@ -186,6 +220,14 @@ class FixtureBuilder { }); } + withShowFiatTestnetEnabled() { + return this.withPreferencesController({ + preferences: { + showFiatInTestnets: true, + }, + }); + } + withConversionRateEnabled() { return this.withPreferencesController({ useCurrencyRateCheck: true, @@ -391,6 +433,10 @@ class FixtureBuilder { extensionSupport: false, srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }; return this; @@ -594,12 +640,28 @@ class FixtureBuilder { }); } + withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() { + return this.withPreferencesController({ + preferences: { + showNativeTokenAsMainBalance: false, + }, + }); + } + withPreferencesControllerTxSimulationsDisabled() { return this.withPreferencesController({ useTransactionSimulations: false, }); } + withPreferencesControllerSmartTransactionsOptedIn() { + return this.withPreferencesController({ + preferences: { + smartTransactionsOptInStatus: true, + }, + }); + } + withPreferencesControllerAndFeatureFlag(flags) { merge(this.fixture.data.PreferencesController, flags); return this; diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts index 5f0277c191de..f32a48d9c4a8 100644 --- a/test/e2e/flask/btc/btc-account-overview.spec.ts +++ b/test/e2e/flask/btc/btc-account-overview.spec.ts @@ -1,4 +1,6 @@ +import { strict as assert } from 'assert'; import { Suite } from 'mocha'; +import { DEFAULT_BTC_BALANCE } from '../../constants'; import { withBtcAccountSnap } from './common-btc'; describe('BTC Account - Overview', function (this: Suite) { @@ -14,7 +16,7 @@ describe('BTC Account - Overview', function (this: Suite) { await driver.waitForSelector({ text: 'Send', tag: 'button', - css: '[disabled]', + css: '[data-testid="coin-overview-send"]', }); await driver.waitForSelector({ @@ -39,4 +41,23 @@ describe('BTC Account - Overview', function (this: Suite) { }, ); }); + + it('has balance', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + // Wait for the balance to load up + await driver.delay(2000); + + const balanceElement = await driver.findElement( + '.coin-overview__balance', + ); + const balanceText = await balanceElement.getText(); + + const [balance, unit] = balanceText.split('\n'); + assert(Number(balance) === DEFAULT_BTC_BALANCE); + assert(unit === 'BTC'); + }, + ); + }); }); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index a33ab1241a1c..6891b3bfd60e 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -1,34 +1,57 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures, unlockWallet } from '../../helpers'; -import { DEFAULT_BTC_ACCOUNT } from '../../constants'; +import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { Driver } from '../../webdriver/driver'; -import { createBtcAccount } from '../../accounts/common'; +import messages from '../../../../app/_locales/en/messages.json'; -const GENERATE_MOCK_BTC_BALANCE_CALL = ( - address: string = DEFAULT_BTC_ACCOUNT, -): { data: { [address: string]: number } } => { - return { - data: { - [address]: 9999, +export async function createBtcAccount(driver: Driver) { + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement({ + text: messages.addNewBitcoinAccount.message, + tag: 'button', + }); + await driver.clickElementAndWaitToDisappear( + { + text: 'Add account', + tag: 'button', }, - }; -}; + // Longer timeout than usual, this reduces the flakiness + // around Bitcoin account creation (mainly required for + // Firefox) + 5000, + ); +} export async function mockBtcBalanceQuote( mockServer: Mockttp, address: string = DEFAULT_BTC_ACCOUNT, ) { return await mockServer - .forGet(/https:\/\/api\.blockchair\.com\/bitcoin\/addresses\/balances/u) - .withQuery({ - addresses: address, + .forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u) + .withJsonBodyIncluding({ + method: 'bb_getaddress', }) - .thenCallback(() => ({ - statusCode: 200, - json: GENERATE_MOCK_BTC_BALANCE_CALL(address), - })); + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: { + address, + balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats + totalReceived: '0', + totalSent: '0', + unconfirmedBalance: '0', + unconfirmedTxs: 0, + txs: 0, + }, + }, + }; + }); } export async function mockRampsDynamicFeatureFlag( diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index a6031a956a37..1b10599bf5ca 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -10,8 +10,7 @@ import { removeSelectedAccount, tapAndHoldToRevealSRP, } from '../../helpers'; -import { createBtcAccount } from '../../accounts/common'; -import { withBtcAccountSnap } from './common-btc'; +import { createBtcAccount, withBtcAccountSnap } from './common-btc'; describe('Create BTC Account', function (this: Suite) { it('create BTC account from the menu', async function () { @@ -135,11 +134,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..c857838f0810 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -535,7 +535,10 @@ const onboardingRevealAndConfirmSRP = async (driver) => { await driver.clickElement('[data-testid="confirm-recovery-phrase"]'); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Confirm', + }); }; /** @@ -546,21 +549,43 @@ 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' }); - // opt-out from third party API - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); + + // opt-out from third party API on general section + await driver.clickElementAndWaitToDisappear({ + text: 'Manage default privacy settings', + tag: 'button', + }); + await driver.clickElement({ text: 'General', tag: 'p' }); 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.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Turn off', + }); + // opt-out from third party API on assets section + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.clickElement({ text: 'Assets', tag: 'p' }); await Promise.all( ( await driver.findClickableElements( @@ -568,19 +593,21 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { ) ).map((toggle) => toggle.click()), ); - // complete onboarding - await driver.clickElement({ text: 'Done', tag: 'button' }); -}; + await driver.clickElement('[data-testid="category-back-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"]'); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); + await driver.clickElement('[data-testid="privacy-settings-back-button"]'); + + // complete onboarding + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Done', + }); + await onboardingPinExtension(driver); }; const completeCreateNewWalletOnboardingFlowWithOptOut = async ( @@ -755,12 +782,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); @@ -1189,10 +1223,7 @@ async function tempToggleSettingRedesignedConfirmations(driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); + await driver.clickElement('[data-testid="account-options-menu-button"]'); // fix race condition with mmi build if (process.env.MMI) { 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/manifest-flag-mocha-hooks.ts b/test/e2e/manifest-flag-mocha-hooks.ts index f319984eb067..f5c8d5e8099c 100644 --- a/test/e2e/manifest-flag-mocha-hooks.ts +++ b/test/e2e/manifest-flag-mocha-hooks.ts @@ -14,7 +14,9 @@ */ import fs from 'fs'; import { hasProperty } from '@metamask/utils'; -import { folder } from './set-manifest-flags'; +import { folder, getManifestVersion } from './set-manifest-flags'; + +process.env.ENABLE_MV3 = getManifestVersion() === 3 ? 'true' : 'false'; // Global beforeEach hook to backup the manifest.json file if (typeof beforeEach === 'function' && process.env.SELENIUM_BROWSER) { diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 929a424a36ef..12d0fb293e15 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, @@ -62,6 +66,19 @@ const emptyHtmlPage = () => ` const browserAPIRequestDomains = /^.*\.(googleapis\.com|google\.com|mozilla\.net|mozilla\.com|mozilla\.org|gvt1\.com)$/iu; +/** + * Some third-party providers might use random URLs that we don't want to track + * in the privacy report "in clear". We identify those private hosts with a + * `pattern` regexp and replace the original host by a more generic one (`host`). + * For example, "my-secret-host.provider.com" could be denoted as "*.provider.com" in + * the privacy report. This would prevent disclosing the "my-secret-host" subdomain + * in this case. + */ +const privateHostMatchers = [ + // { pattern: RegExp, host: string } + { pattern: /^.*\.btc.*\.quiknode.pro$/iu, host: '*.btc*.quiknode.pro' }, +]; + /** * @typedef {import('mockttp').Mockttp} Mockttp * @typedef {import('mockttp').MockedEndpoint} MockedEndpoint @@ -268,35 +285,33 @@ async function setupMocking( .thenCallback(() => { return { statusCode: 200, - json: [ - { - ethereum: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - bsc: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - polygon: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - avalanche: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - smartTransactions: { - mobileActive: false, - extensionActive: false, - }, - updated_at: '2022-03-17T15:54:00.360Z', + json: { + ethereum: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, }, - ], + bsc: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + polygon: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + avalanche: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + smartTransactions: { + mobileActive: false, + extensionActive: true, + }, + updated_at: '2022-03-17T15:54:00.360Z', + }, }; }); @@ -313,6 +328,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(() => { @@ -420,7 +481,7 @@ async function setupMocking( decimals: 18, name: 'Dai Stablecoin', iconUrl: - 'https://crypto.com/price/coin-data/icon/DAI/color_icon.png', + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', type: 'erc20', aggregators: [ 'aave', @@ -447,7 +508,7 @@ async function setupMocking( decimals: 6, name: 'USD Coin', iconUrl: - 'https://crypto.com/price/coin-data/icon/USDC/color_icon.png', + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', type: 'erc20', aggregators: [ 'aave', @@ -664,6 +725,25 @@ async function setupMocking( const portfolioRequestsMatcher = (request) => request.headers.referer === 'https://portfolio.metamask.io/'; + /** + * Tests a request against private domains and returns a set of generic hostnames that + * match. + * + * @param request + * @returns A set of matched results. + */ + const matchPrivateHosts = (request) => { + const privateHosts = new Set(); + + for (const { pattern, host: privateHost } of privateHostMatchers) { + if (request.headers.host.match(pattern)) { + privateHosts.add(privateHost); + } + } + + return privateHosts; + }; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -673,6 +753,16 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { + const privateHosts = matchPrivateHosts(request); + if (privateHosts.size) { + for (const privateHost of privateHosts) { + privacyReport.add(privateHost); + } + // At this point, we know the request at least one private doamin, so we just stops here to avoid + // using the request any further. + return; + } + if ( request.headers.host.match(browserAPIRequestDomains) === null && !portfolioRequestsMatcher(request) 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/send-transaction.flow.ts b/test/e2e/page-objects/flows/send-transaction.flow.ts index 8291dc96a4e6..1cffe2428873 100644 --- a/test/e2e/page-objects/flows/send-transaction.flow.ts +++ b/test/e2e/page-objects/flows/send-transaction.flow.ts @@ -2,6 +2,7 @@ import HomePage from '../pages/homepage'; import ConfirmTxPage from '../pages/send/confirm-tx-page'; import SendTokenPage from '../pages/send/send-token-page'; import { Driver } from '../../webdriver/driver'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; /** * This function initiates the steps required to send a transaction from the homepage to final confirmation. @@ -37,7 +38,32 @@ export const sendTransaction = async ( const confirmTxPage = new ConfirmTxPage(driver); await confirmTxPage.check_pageIsLoaded(gasfee, totalfee); await confirmTxPage.confirmTx(); +}; - // user should land on homepage after transaction is confirmed - await homePage.check_pageIsLoaded(); +/** + * This function initiates the steps required to send a transaction from snap account on homepage to final confirmation. + * + * @param driver - The webdriver instance. + * @param recipientAddress - The recipient address. + * @param amount - The amount of the asset to be sent in the transaction. + * @param gasfee - The expected transaction gas fee. + * @param totalfee - The expected total transaction fee. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const sendTransactionWithSnapAccount = async ( + driver: Driver, + recipientAddress: string, + amount: string, + gasfee: string, + totalfee: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + await sendTransaction(driver, recipientAddress, amount, gasfee, totalfee); + if (!isSyncFlow) { + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + ); + } }; diff --git a/test/e2e/page-objects/flows/sign.flow.ts b/test/e2e/page-objects/flows/sign.flow.ts new file mode 100644 index 000000000000..c7d03bb4f96e --- /dev/null +++ b/test/e2e/page-objects/flows/sign.flow.ts @@ -0,0 +1,169 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import TestDapp from '../pages/test-dapp'; + +/** + * This function initiates the steps for a personal sign with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const personalSignWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.personalSign(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successPersonalSign(publicAddress); + } else { + await testDapp.check_failedPersonalSign( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedData with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedData(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedData(publicAddress); + } else { + await testDapp.check_failedSignTypedData( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV3 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV3WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV3(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV3(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV3( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV4 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV4WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV4(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV4(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV4( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signPermit with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signPermitWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signPermit(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignPermit(publicAddress); + } else { + await testDapp.check_failedSignPermit( + 'Error: Request rejected by user or snap.', + ); + } +}; diff --git a/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts new file mode 100644 index 000000000000..76016a9c370f --- /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..7218c727a929 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,69 +1,61 @@ import { Driver } from '../../webdriver/driver'; class AccountListPage { - private driver: Driver; + private readonly driver: Driver; - private accountListItem: string; + private readonly accountListBalance = + '[data-testid="second-currency-display"]'; - private accountMenuButton: string; + private readonly accountListItem = + '.multichain-account-menu-popover__list--menu-item'; - private accountNameInput: string; + private readonly accountMenuButton = + '[data-testid="account-list-menu-details"]'; - private accountOptionsMenuButton: string; + private readonly accountNameInput = '#account-name'; - private addAccountConfirmButton: string; + private readonly accountOptionsMenuButton = + '[data-testid="account-list-item-menu-button"]'; - private addEthereumAccountButton: string; + private readonly addAccountConfirmButton = + '[data-testid="submit-add-account-with-name"]'; - private addSnapAccountButton: object; + private readonly addEthereumAccountButton = + '[data-testid="multichain-account-menu-popover-add-account"]'; - private closeAccountModalButton: string; + private readonly addSnapAccountButton = { + text: 'Add account Snap', + tag: 'button', + }; - private createAccountButton: string; + private readonly closeAccountModalButton = 'button[aria-label="Close"]'; - private editableLabelButton: string; + private readonly createAccountButton = + '[data-testid="multichain-account-menu-popover-action-button"]'; - private editableLabelInput: string; + private readonly editableLabelButton = + '[data-testid="editable-label-button"]'; - private hideUnhideAccountButton: string; + private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private hiddenAccountOptionsMenuButton: string; + private readonly hideUnhideAccountButton = + '[data-testid="account-list-menu-hide"]'; - private hiddenAccountsList: string; + private readonly hiddenAccountOptionsMenuButton = + '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - private pinUnpinAccountButton: string; + private readonly hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - private pinnedIcon: string; + private readonly pinUnpinAccountButton = + '[data-testid="account-list-menu-pin"]'; - private saveAccountLabelButton: string; + private readonly pinnedIcon = '[data-testid="account-pinned-icon"]'; + + 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 { @@ -167,6 +159,21 @@ class AccountListPage { await this.driver.clickElement(this.pinUnpinAccountButton); } + /** + * Checks that the account balance is displayed in the account list. + * + * @param expectedBalance - The expected balance to check. + */ + async check_accountBalanceDisplayed(expectedBalance: string): Promise { + console.log( + `Check that account balance ${expectedBalance} is displayed in account list`, + ); + await this.driver.waitForSelector({ + css: this.accountListBalance, + text: expectedBalance, + }); + } + async check_accountDisplayedInAccountList( expectedLabel: string = 'Account', ): Promise { @@ -179,9 +186,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/confirmations/legacy/watch-asset-confirmation.ts b/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts new file mode 100644 index 000000000000..23f1b010de87 --- /dev/null +++ b/test/e2e/page-objects/pages/confirmations/legacy/watch-asset-confirmation.ts @@ -0,0 +1,20 @@ +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; + +class WatchAssetConfirmation { + private driver: Driver; + + private footerConfirmButton: RawLocator; + + constructor(driver: Driver) { + this.driver = driver; + + this.footerConfirmButton = '[data-testid="page-container-footer-next"]'; + } + + async clickFooterConfirmButton() { + await this.driver.clickElement(this.footerConfirmButton); + } +} + +export default WatchAssetConfirmation; diff --git a/test/e2e/page-objects/pages/confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts similarity index 85% rename from test/e2e/page-objects/pages/confirmation.ts rename to test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts index 3ec372cb3163..f8fc66c3fc65 100644 --- a/test/e2e/page-objects/pages/confirmation.ts +++ b/test/e2e/page-objects/pages/confirmations/redesign/confirmation.ts @@ -1,5 +1,5 @@ -import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; class Confirmation { protected driver: Driver; diff --git a/test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts similarity index 89% rename from test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts rename to test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts index 5259e0a51dcd..a1aadeff3376 100644 --- a/test/e2e/page-objects/pages/set-approval-for-all-transaction-confirmation.ts +++ b/test/e2e/page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation.ts @@ -1,6 +1,6 @@ -import { tEn } from '../../../lib/i18n-helpers'; -import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { tEn } from '../../../../../lib/i18n-helpers'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; import TransactionConfirmation from './transaction-confirmation'; class SetApprovalForAllTransactionConfirmation extends TransactionConfirmation { diff --git a/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts new file mode 100644 index 000000000000..837c7aa24e21 --- /dev/null +++ b/test/e2e/page-objects/pages/confirmations/redesign/token-transfer-confirmation.ts @@ -0,0 +1,45 @@ +import { tEn } from '../../../../../lib/i18n-helpers'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; +import TransactionConfirmation from './transaction-confirmation'; + +class TokenTransferTransactionConfirmation extends TransactionConfirmation { + private networkParagraph: RawLocator; + + private interactingWithParagraph: RawLocator; + + private networkFeeParagraph: RawLocator; + + constructor(driver: Driver) { + super(driver); + + this.driver = driver; + + this.networkParagraph = { + css: 'p', + text: tEn('transactionFlowNetwork') as string, + }; + this.interactingWithParagraph = { + css: 'p', + text: tEn('interactingWith') as string, + }; + this.networkFeeParagraph = { + css: 'p', + text: tEn('networkFee') as string, + }; + } + + async check_networkParagraph() { + await this.driver.waitForSelector(this.networkParagraph); + } + + async check_interactingWithParagraph() { + await this.driver.waitForSelector(this.interactingWithParagraph); + } + + async check_networkFeeParagraph() { + await this.driver.waitForSelector(this.networkFeeParagraph); + } +} + +export default TokenTransferTransactionConfirmation; diff --git a/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts new file mode 100644 index 000000000000..c7f618d3fc61 --- /dev/null +++ b/test/e2e/page-objects/pages/confirmations/redesign/transaction-confirmation.ts @@ -0,0 +1,35 @@ +import { tEn } from '../../../../../lib/i18n-helpers'; +import { Driver } from '../../../../webdriver/driver'; +import { RawLocator } from '../../../common'; +import Confirmation from './confirmation'; + +class TransactionConfirmation extends Confirmation { + private walletInitiatedHeadingTitle: RawLocator; + + private dappInitiatedHeadingTitle: RawLocator; + + constructor(driver: Driver) { + super(driver); + + this.driver = driver; + + this.walletInitiatedHeadingTitle = { + css: 'h3', + text: tEn('review') as string, + }; + this.dappInitiatedHeadingTitle = { + css: 'h3', + text: tEn('transferRequest') as string, + }; + } + + async check_walletInitiatedHeadingTitle() { + await this.driver.waitForSelector(this.walletInitiatedHeadingTitle); + } + + async check_dappInitiatedHeadingTitle() { + await this.driver.waitForSelector(this.dappInitiatedHeadingTitle); + } +} + +export default TransactionConfirmation; diff --git a/test/e2e/page-objects/pages/experimental-settings.ts b/test/e2e/page-objects/pages/experimental-settings.ts index 8c7129b17555..7cd780229acd 100644 --- a/test/e2e/page-objects/pages/experimental-settings.ts +++ b/test/e2e/page-objects/pages/experimental-settings.ts @@ -9,9 +9,12 @@ class ExperimentalSettings { private readonly experimentalPageTitle: object = { text: 'Experimental', - css: '.h4', + tag: 'h4', }; + private readonly redesignedSignatureToggle: string = + '[data-testid="toggle-redesigned-confirmations-container"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -33,6 +36,11 @@ class ExperimentalSettings { console.log('Toggle Add Account Snap on experimental setting page'); await this.driver.clickElement(this.addAccountSnapToggle); } + + async toggleRedesignedSignature(): Promise { + console.log('Toggle Redesigned Signature on experimental setting page'); + await this.driver.clickElement(this.redesignedSignatureToggle); + } } export default ExperimentalSettings; diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 742b37a5c48f..8bd29ea8c602 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -3,24 +3,43 @@ import { Driver } from '../../webdriver/driver'; class HeaderNavbar { private driver: Driver; - private accountMenuButton: string; + private readonly accountMenuButton = '[data-testid="account-menu-icon"]'; - private accountOptionMenu: string; + private readonly accountOptionMenu = + '[data-testid="account-options-menu-button"]'; - private lockMetaMaskButton: string; + private readonly accountSnapButton = { text: 'Snaps', tag: 'div' }; - private settingsButton: string; + private readonly lockMetaMaskButton = '[data-testid="global-menu-lock"]'; + + private readonly mmiPortfolioButton = + '[data-testid="global-menu-mmi-portfolio"]'; + + private readonly settingsButton = '[data-testid="global-menu-settings"]'; 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.settingsButton = '[data-testid="global-menu-settings"]'; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.accountMenuButton, + this.accountOptionMenu, + ]); + } catch (e) { + console.log('Timeout while waiting for header navbar to be loaded', e); + throw e; + } + console.log('Header navbar is loaded'); } 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 +47,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..326ecc3188b7 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -6,36 +6,35 @@ import HeaderNavbar from './header-navbar'; class HomePage { private driver: Driver; - private sendButton: string; + public headerNavbar: HeaderNavbar; - private activityTab: string; + private readonly activityTab = + '[data-testid="account-overview__activity-tab"]'; - private tokensTab: string; + private readonly balance = '[data-testid="eth-overview__primary-currency"]'; - private balance: string; + private readonly completedTransactions = '[data-testid="activity-list-item"]'; - private completedTransactions: string; + private readonly confirmedTransactions = { + text: 'Confirmed', + css: '.transaction-status-label--confirmed', + }; - private confirmedTransactions: object; + private readonly failedTransactions = { + text: 'Failed', + css: '.transaction-status-label--failed', + }; - private transactionAmountsInActivity: string; + private readonly sendButton = '[data-testid="eth-overview-send"]'; - public headerNavbar: HeaderNavbar; + private readonly tokensTab = '[data-testid="account-overview__asset-tab"]'; + + private readonly transactionAmountsInActivity = + '[data-testid="transaction-list-item-primary-currency"]'; constructor(driver: Driver) { this.driver = driver; this.headerNavbar = new HeaderNavbar(driver); - this.sendButton = '[data-testid="eth-overview-send"]'; - this.activityTab = '[data-testid="account-overview__activity-tab"]'; - this.tokensTab = '[data-testid="account-overview__asset-tab"]'; - this.confirmedTransactions = { - text: 'Confirmed', - css: '.transaction-status-label--confirmed', - }; - this.balance = '[data-testid="eth-overview__primary-currency"]'; - this.completedTransactions = '[data-testid="activity-list-item"]'; - this.transactionAmountsInActivity = - '[data-testid="transaction-list-item-primary-currency"]'; } async check_pageIsLoaded(): Promise { @@ -109,8 +108,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({ @@ -129,6 +133,28 @@ class HomePage { ); } + /** + * This function checks if the specified number of failed transactions are displayed in the activity list on homepage. + * It waits up to 10 seconds for the expected number of failed transactions to be visible. + * + * @param expectedNumber - The number of failed transactions expected to be displayed in activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of failed transactions is displayed within the timeout period. + */ + async check_failedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise { + console.log( + `Wait for ${expectedNumber} failed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const failedTxs = await this.driver.findElements(this.failedTransactions); + return failedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} failed transactions found in activity list on homepage`, + ); + } + async check_ganacheBalanceIsDisplayed( ganacheServer?: Ganache, address = null, diff --git a/test/e2e/page-objects/pages/send/send-token-page.ts b/test/e2e/page-objects/pages/send/send-token-page.ts index 65727b106783..728afbfdd4df 100644 --- a/test/e2e/page-objects/pages/send/send-token-page.ts +++ b/test/e2e/page-objects/pages/send/send-token-page.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; import { Driver } from '../../../webdriver/driver'; +import { RawLocator } from '../../common'; class SendTokenPage { private driver: Driver; @@ -18,6 +19,10 @@ class SendTokenPage { private ensResolvedAddress: string; + private assetPickerButton: RawLocator; + + private tokenListButton: RawLocator; + constructor(driver: Driver) { this.driver = driver; this.inputAmount = '[data-testid="currency-input"]'; @@ -32,6 +37,8 @@ class SendTokenPage { text: 'Continue', tag: 'button', }; + this.assetPickerButton = '[data-testid="asset-picker-button"]'; + this.tokenListButton = '[data-testid="multichain-token-list-button"]'; } async check_pageIsLoaded(): Promise { @@ -125,6 +132,15 @@ class SendTokenPage { `ENS domain '${ensDomain}' resolved to address '${address}' and can be used as recipient on send token screen.`, ); } + + async click_assetPickerButton() { + await this.driver.clickElement(this.assetPickerButton); + } + + async click_secondTokenListButton() { + const elements = await this.driver.findElements(this.tokenListButton); + await elements[1].click(); + } } export default SendTokenPage; 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..c75adb06da3a --- /dev/null +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -0,0 +1,362 @@ +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 addtoMetamaskMessage = { + text: 'Add to MetaMask', + tag: 'h3', + }; + + private readonly approveRequestButton = { + text: 'Approve Request', + tag: 'button', + }; + + private readonly approveRequestIdInput = '#approve-request-request-id'; + + private readonly approveRequestSection = { + text: 'Approve request', + tag: 'div', + }; + + 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 importAccountButton = { + text: 'Import Account', + tag: 'button', + }; + + private readonly importAccountPrivateKeyInput = '#import-account-private-key'; + + private readonly importAccountSection = { + text: 'Import account', + tag: 'div', + }; + + private readonly installationCompleteMessage = { + text: 'Installation complete', + tag: 'h2', + }; + + private readonly listRequestsButton = { + text: 'List Requests', + tag: 'button', + }; + + private readonly listRequestsSection = { + text: 'List requests', + tag: 'div', + }; + + private readonly newAccountMessage = { + text: '"address":', + tag: 'div', + }; + + private readonly pageTitle = { + text: 'Snap Simple Keyring', + tag: 'p', + }; + + private readonly rejectRequestButton = { + text: 'Reject Request', + tag: 'button', + }; + + private readonly rejectRequestIdInput = '#reject-request-request-id'; + + private readonly rejectRequestSection = { + text: 'Reject request', + tag: 'div', + }; + + private readonly requestMessage = { + text: '"scope":', + tag: 'div', + }; + + 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'); + } + + /** + * Approves or rejects a transaction from a snap account on Snap Simple Keyring page. + * + * @param approveTransaction - Indicates if the transaction should be approved. Defaults to true. + * @param isSignatureRequest - Indicates if the request is a signature request. Defaults to false. + */ + async approveRejectSnapAccountTransaction( + approveTransaction: boolean = true, + isSignatureRequest: boolean = false, + ): Promise { + console.log( + 'Approve/Reject snap account transaction on Snap Simple Keyring page', + ); + if (isSignatureRequest) { + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + } else { + // For send eth requests, the origin screen is not closed automatically, so we cannot call clickElementAndWaitForWindowToClose here. + await this.driver.clickElementAndWaitToDisappear( + this.confirmationSubmitButton, + ); + } + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + + // Get the first request from the requests list on simple keyring snap page + await this.driver.clickElementUsingMouseMove(this.listRequestsSection); + await this.driver.clickElement(this.listRequestsButton); + const requestJSON = await ( + await this.driver.waitForSelector(this.requestMessage) + ).getText(); + + if (approveTransaction) { + console.log( + 'Approve snap account transaction on Snap Simple Keyring page', + ); + await this.driver.clickElementUsingMouseMove(this.approveRequestSection); + await this.driver.fill( + this.approveRequestIdInput, + JSON.parse(requestJSON)[0].id, + ); + await this.driver.clickElement(this.approveRequestButton); + } else { + console.log( + 'Reject snap account transaction on Snap Simple Keyring page', + ); + await this.driver.clickElementUsingMouseMove(this.rejectRequestSection); + await this.driver.fill( + this.rejectRequestIdInput, + JSON.parse(requestJSON)[0].id, + ); + await this.driver.clickElement(this.rejectRequestButton); + } + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + } + + 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, + ); + } + + /** + * Confirms the add account dialog on Snap Simple Keyring page. + * + * @param accountName - Optional: name for the snap account. Defaults to "SSK Account". + */ + async confirmAddAccountDialog( + accountName: string = 'SSK Account', + ): Promise { + console.log('Confirm add account dialog'); + 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.driver.waitForSelector(this.newAccountMessage); + } + + 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. + * @returns the public key of the new created account + */ + 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.confirmAddAccountDialog(accountName); + const newAccountJSONMessage = await ( + await this.driver.waitForSelector(this.newAccountMessage) + ).getText(); + const newPublicKey = JSON.parse(newAccountJSONMessage).address; + return newPublicKey; + } + + /** + * Imports an account with a private key on Snap Simple Keyring page. + * + * @param privateKey - The private key to import. + */ + async importAccountWithPrivateKey(privateKey: string): Promise { + console.log('Import account with private key on Snap Simple Keyring page'); + await this.driver.clickElement(this.importAccountSection); + await this.driver.fill(this.importAccountPrivateKeyInput, privateKey); + await this.driver.clickElement(this.importAccountButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.confirmCreateSnapOnConfirmationScreen(); + await this.confirmAddAccountDialog(); + } + + /** + * 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_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/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 89ee6bc9cbd3..4a02d80459e0 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -1,5 +1,5 @@ +import { WINDOW_TITLES } from '../../helpers'; import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; @@ -7,40 +7,167 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; class TestDapp { private driver: Driver; - private erc721SetApprovalForAllButton: RawLocator; + private readonly confirmDepositButton = + '[data-testid="confirm-footer-button"]'; - private erc1155SetApprovalForAllButton: RawLocator; + private readonly confirmDialogButton = '[data-testid="confirm-btn"]'; - private erc721RevokeSetApprovalForAllButton: RawLocator; + private readonly confirmDialogScrollButton = + '[data-testid="signature-request-scroll-button"]'; - private erc1155RevokeSetApprovalForAllButton: RawLocator; + private readonly confirmSignatureButton = + '[data-testid="page-container-footer-next"]'; + + private readonly connectAccountButton = '#connectButton'; + + private readonly connectMetaMaskMessage = { + text: 'Connect with MetaMask', + tag: 'h2', + }; + + private readonly connectedAccount = '#accounts'; + + private readonly depositPiggyBankContractButton = '#depositButton'; + + private readonly editConnectButton = { + text: 'Edit', + tag: 'button', + }; + + private readonly erc1155RevokeSetApprovalForAllButton = + '#revokeERC1155Button'; + + private readonly erc1155SetApprovalForAllButton = + '#setApprovalForAllERC1155Button'; + + private readonly erc20WatchAssetButton = '#watchAssets'; + + private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton'; + + private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton'; + + private readonly localhostCheckbox = { + text: 'Localhost 8545', + tag: 'p', + }; + + private readonly localhostNetworkMessage = { + css: '#chainId', + text: '0x539', + }; + + private readonly mmlogo = '#mm-logo'; + + private readonly personalSignButton = '#personalSign'; + + private readonly personalSignResult = '#personalSignVerifyECRecoverResult'; + + private readonly personalSignSignatureRequestMessage = { + text: 'personal_sign', + tag: 'div', + }; + + private readonly personalSignVerifyButton = '#personalSignVerify'; + + private readonly revokePermissionButton = '#revokeAccountsPermission'; + + private readonly signPermitButton = '#signPermit'; + + private readonly signPermitResult = '#signPermitResult'; + + private readonly signPermitSignatureRequestMessage = { + text: 'Permit', + tag: 'p', + }; + + private readonly signPermitVerifyButton = '#signPermitVerify'; + + private readonly signPermitVerifyResult = '#signPermitVerifyResult'; + + private readonly signTypedDataButton = '#signTypedData'; + + private readonly signTypedDataResult = '#signTypedDataResult'; + + private readonly signTypedDataSignatureRequestMessage = { + text: 'Hi, Alice!', + tag: 'div', + }; + + private readonly signTypedDataV3Button = '#signTypedDataV3'; + + private readonly signTypedDataV3Result = '#signTypedDataV3Result'; + + private readonly signTypedDataV3V4SignatureRequestMessage = { + text: 'Hello, Bob!', + tag: 'div', + }; + + private readonly signTypedDataV3VerifyButton = '#signTypedDataV3Verify'; + + private readonly signTypedDataV3VerifyResult = '#signTypedDataV3VerifyResult'; + + private readonly signTypedDataV4Button = '#signTypedDataV4'; + + private readonly signTypedDataV4Result = '#signTypedDataV4Result'; + + private readonly signTypedDataV4VerifyButton = '#signTypedDataV4Verify'; + + private readonly signTypedDataV4VerifyResult = '#signTypedDataV4VerifyResult'; + + private readonly signTypedDataVerifyButton = '#signTypedDataVerify'; + + private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult'; + + private readonly transactionRequestMessage = { + text: 'Transaction request', + tag: 'h2', + }; + + private readonly updateNetworkButton = { + text: 'Update', + tag: 'button', + }; + + private erc20TokenTransferButton = '#transferTokens'; constructor(driver: Driver) { this.driver = driver; + } - this.erc721SetApprovalForAllButton = '#setApprovalForAllButton'; - this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; - this.erc721RevokeSetApprovalForAllButton = '#revokeButton'; - this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.mmlogo); + } catch (e) { + console.log('Timeout while waiting for Test Dapp page to be loaded', e); + throw e; + } + console.log('Test Dapp page is loaded'); } - async open({ - contractAddress, + /** + * Open the test dapp page. + * + * @param options - The options for opening the test dapp page. + * @param options.contractAddress - The contract address to open the dapp with. Defaults to null. + * @param options.url - The URL of the dapp. Defaults to DAPP_URL. + * @returns A promise that resolves when the new page is opened. + */ + async openTestDappPage({ + contractAddress = null, url = DAPP_URL, }: { - contractAddress?: string; + contractAddress?: string | null; url?: string; - }) { + } = {}): Promise { const dappUrl = contractAddress ? `${url}/?contract=${contractAddress}` : url; - - return await this.driver.openNewPage(dappUrl); + await this.driver.openNewPage(dappUrl); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async request(method: string, params: any[]) { - await this.open({ + await this.openTestDappPage({ url: `${DAPP_URL}/request?method=${method}¶ms=${JSON.stringify( params, )}`, @@ -55,13 +182,295 @@ class TestDapp { await this.driver.clickElement(this.erc1155SetApprovalForAllButton); } - public async clickERC721RevokeSetApprovalForAllButton() { + async clickERC721RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc721RevokeSetApprovalForAllButton); } - public async clickERC1155RevokeSetApprovalForAllButton() { + async clickERC1155RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton); } -} + public async clickERC20WatchAssetButton() { + await this.driver.clickElement(this.erc20WatchAssetButton); + } + + public async clickERC20TokenTransferButton() { + await this.driver.clickElement(this.erc20TokenTransferButton); + } + + /** + * Connect account to test dapp. + * + * @param publicAddress - The public address to connect to test dapp. + */ + async connectAccount(publicAddress: string) { + console.log('Connect account to test dapp'); + await this.driver.clickElement(this.connectAccountButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.connectMetaMaskMessage); + + // TODO: Extra steps needed to preserve the current network. + // Following steps can be removed once the issue is fixed (#27891) + const editNetworkButton = await this.driver.findClickableElements( + this.editConnectButton, + ); + await editNetworkButton[1].click(); + await this.driver.clickElement(this.localhostCheckbox); + await this.driver.clickElement(this.updateNetworkButton); + + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmDialogButton, + ); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.connectedAccount, + text: publicAddress.toLowerCase(), + }); + await this.driver.waitForSelector(this.localhostNetworkMessage); + } + + async createDepositTransaction() { + console.log('Create a deposit transaction on test dapp page'); + await this.driver.clickElement(this.depositPiggyBankContractButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.transactionRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmDepositButton, + ); + } + + /** + * Disconnect current connected account from test dapp. + * + * @param publicAddress - The public address of the account to disconnect from test dapp. + */ + async disconnectAccount(publicAddress: string) { + console.log('Disconnect account from test dapp'); + await this.driver.clickElement(this.revokePermissionButton); + await this.driver.refresh(); + await this.check_pageIsLoaded(); + await this.driver.assertElementNotPresent({ + css: this.connectedAccount, + text: publicAddress.toLowerCase(), + }); + } + + /** + * Verify the failed personal sign signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedPersonalSign(expectedFailedMessage: string) { + console.log('Verify failed personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.personalSignButton, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signPermit signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignPermit(expectedFailedMessage: string) { + console.log('Verify failed signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signPermitResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedData signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedData(expectedFailedMessage: string) { + console.log('Verify failed signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV3 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV3(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV3Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV4 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV4(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV4Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the successful personal sign signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successPersonalSign(publicKey: string) { + console.log('Verify successful personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.personalSignVerifyButton); + await this.driver.waitForSelector({ + css: this.personalSignResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signPermit signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignPermit(publicKey: string) { + console.log('Verify successful signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signPermitVerifyButton); + await this.driver.waitForSelector({ + css: this.signPermitVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedData signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedData(publicKey: string) { + console.log('Verify successful signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataVerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV3 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV3(publicKey: string) { + console.log('Verify successful signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV3VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV3VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV4 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV4(publicKey: string) { + console.log('Verify successful signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV4VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV4VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Sign a message with the personal sign method. + */ + async personalSign() { + console.log('Sign message with personal sign'); + await this.driver.clickElement(this.personalSignButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.personalSignSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign message with the signPermit method. + */ + async signPermit() { + console.log('Sign message with signPermit'); + await this.driver.clickElement(this.signPermitButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.signPermitSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedData method. + */ + async signTypedData() { + console.log('Sign message with signTypedData'); + await this.driver.clickElement(this.signTypedDataButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataSignatureRequestMessage, + ); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV3 method. + */ + async signTypedDataV3() { + console.log('Sign message with signTypedDataV3'); + await this.driver.clickElement(this.signTypedDataV3Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV4 method. + */ + async signTypedDataV4() { + console.log('Sign message with signTypedDataV4'); + await this.driver.clickElement(this.signTypedDataV4Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } +} export default TestDapp; diff --git a/test/e2e/page-objects/pages/transaction-confirmation.ts b/test/e2e/page-objects/pages/transaction-confirmation.ts deleted file mode 100644 index 7ae98d74d4c8..000000000000 --- a/test/e2e/page-objects/pages/transaction-confirmation.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Confirmation from './confirmation'; - -class TransactionConfirmation extends Confirmation {} - -export default TransactionConfirmation; diff --git a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts index a9a175e82971..cf455dc0a7e0 100644 --- a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts +++ b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts @@ -34,10 +34,11 @@ export class DummyAppPage { this.connectBtn.click(), ]); await popup1.waitForLoadState(); - // Check which account is selected and select if required - await popup1.locator('.check-box__indeterminate'); - await popup1.locator('button:has-text("Next")').click(); - await popup1.locator('button:has-text("Confirm")').click(); + await popup1.getByTestId('edit').nth(1).click(); + await popup1.getByText('Select all').click(); + await popup1.getByTestId('Sepolia').click(); + await popup1.getByTestId('connect-more-chains-button').click(); + await popup1.getByTestId('confirm-btn').click(); await popup1.close(); } @@ -60,11 +61,7 @@ export class DummyAppPage { if (isSign) { await popup.click('button:has-text("Confirm")'); } else { - await popup.getByTestId('page-container-footer-next').click(); - - if (buttonId === 'approveTokens') { - await popup.getByTestId('page-container-footer-next').click(); - } + await popup.getByTestId('confirm-footer-button').click(); await popup .getByTestId('custody-confirm-link__btn') 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..75339250506f 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -1,12 +1,95 @@ +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}`; +type ManifestType = { _flags?: ManifestFlags; manifest_version: string }; +let manifest: ManifestType; + 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,13 +103,36 @@ 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( - fs.readFileSync(`${folder}/manifest.json`).toString(), - ); + readManifest(); manifest._flags = flags; fs.writeFileSync(`${folder}/manifest.json`, JSON.stringify(manifest)); } + +export function getManifestVersion(): number { + readManifest(); + + return parseInt(manifest.manifest_version, 10); +} + +function readManifest() { + if (!manifest) { + manifest = JSON.parse( + fs.readFileSync(`${folder}/manifest.json`).toString(), + ); + } +} diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 2b1a6bc6532d..7fdbf5e1fd24 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.1/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.2', }; 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-siginsights.spec.js b/test/e2e/snaps/test-snap-siginsights.spec.js index d40cbc83ae35..b72d6e248ff0 100644 --- a/test/e2e/snaps/test-snap-siginsights.spec.js +++ b/test/e2e/snaps/test-snap-siginsights.spec.js @@ -79,22 +79,15 @@ describe('Test Snap Signature Insights', function () { tag: 'p', }); - // click down arrow - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765', tag: 'p', }); + // Click down arrow + await driver.clickElementSafe('[aria-label="Scroll down"]'); + // click sign button await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-button"]', @@ -125,22 +118,11 @@ describe('Test Snap Signature Insights', function () { }); // click down arrow - // await driver.waitForSelector('[aria-label="Scroll down"]'); await driver.clickElementSafe('[aria-label="Scroll down"]'); // required: delay for scroll to render await driver.delay(500); - // click down arrow - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '1', @@ -188,16 +170,6 @@ describe('Test Snap Signature Insights', function () { // required: delay for scroll to render await driver.delay(500); - // click signature insights - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC has been identified as a malicious verifying contract.', @@ -245,16 +217,6 @@ describe('Test Snap Signature Insights', function () { // required: delay for scroll to render await driver.delay(500); - // click signature insights - await driver.waitForSelector({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - await driver.clickElement({ - text: 'Signature Insights Example Snap', - tag: 'span', - }); - // look for returned signature insights data await driver.waitForSelector({ text: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC has been identified as a malicious verifying contract.', 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..04980cf20c3e 100644 --- a/test/e2e/tests/account/add-account.spec.js +++ b/test/e2e/tests/account/add-account.spec.js @@ -42,6 +42,9 @@ describe('Add account', function () { ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.findElement({ css: '[data-testid="account-menu-icon"]', @@ -74,6 +77,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"]', ); @@ -81,6 +89,9 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); // Check address of 2nd account @@ -104,16 +115,11 @@ describe('Add account', function () { '[data-testid="account-options-menu-button"]', ); - await driver.delay(regularDelayMs); - await driver.waitForSelector('[data-testid="global-menu-lock"]'); await driver.clickElement('[data-testid="global-menu-lock"]'); await driver.waitForSelector('[data-testid="unlock-page"]'); // Recover via SRP in "forget password" option - const restoreSeedLink = await driver.findClickableElement( - '.unlock-page__link', - ); - await restoreSeedLink.click(); + await driver.clickElement('.unlock-page__link'); await driver.pasteIntoField( '[data-testid="import-srp__srp-word-0"]', TEST_SEED_PHRASE, @@ -121,7 +127,6 @@ describe('Add account', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.fill('#confirm-password', 'correct horse battery staple'); - await driver.delay(regularDelayMs); await driver.clickElement( '[data-testid="create-new-vault-submit-button"]', ); @@ -166,6 +171,9 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); // Wait for 2nd account to be created 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/account/snap-account-contract-interaction.spec.ts b/test/e2e/tests/account/snap-account-contract-interaction.spec.ts new file mode 100644 index 000000000000..e4753f5ff05b --- /dev/null +++ b/test/e2e/tests/account/snap-account-contract-interaction.spec.ts @@ -0,0 +1,83 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { Ganache } from '../../seeder/ganache'; +import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry'; +import { + multipleGanacheOptionsForType2Transactions, + PRIVATE_KEY_TWO, + withFixtures, + WINDOW_TITLES, +} from '../../helpers'; +import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Snap Account Contract interaction @no-mmi', function (this: Suite) { + const smartContract = SMART_CONTRACTS.PIGGYBANK; + it('deposits to piggybank contract', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerSnapAccountConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: multipleGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ + driver, + contractRegistry, + ganacheServer, + }: { + driver: Driver; + contractRegistry: GanacheContractAddressRegistry; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // Import snap account with private key on snap simple keyring page. + await snapSimpleKeyringPage.importAccountWithPrivateKey( + PRIVATE_KEY_TWO, + ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Open Dapp with contract + const testDapp = new TestDapp(driver); + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + await testDapp.openTestDappPage({ contractAddress }); + await testDapp.check_pageIsLoaded(); + await testDapp.createDepositTransaction(); + + // Confirm the transaction in activity list on MetaMask + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.goToActivityList(); + await homePage.check_confirmedTxNumberDisplayedInActivity(); + await homePage.check_txAmountInActivity('-4 ETH'); + }, + ); + }); +}); diff --git a/test/e2e/tests/account/snap-account-settings.spec.ts b/test/e2e/tests/account/snap-account-settings.spec.ts index cbd5f8814b7b..1a0c761fb4df 100644 --- a/test/e2e/tests/account/snap-account-settings.spec.ts +++ b/test/e2e/tests/account/snap-account-settings.spec.ts @@ -33,7 +33,7 @@ describe('Add snap account experimental settings @no-mmi', function (this: Suite await settingsPage.goToExperimentalSettings(); const experimentalSettings = new ExperimentalSettings(driver); - await settingsPage.check_pageIsLoaded(); + await experimentalSettings.check_pageIsLoaded(); await experimentalSettings.toggleAddAccountSnap(); // Make sure the "Add account Snap" button is visible. diff --git a/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts new file mode 100644 index 000000000000..7398747671c7 --- /dev/null +++ b/test/e2e/tests/account/snap-account-signatures-and-disconnects.spec.ts @@ -0,0 +1,66 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES, withFixtures } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import ExperimentalSettings from '../../page-objects/pages/experimental-settings'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + signTypedDataV3WithSnapAccount, + signTypedDataV4WithSnapAccount, +} from '../../page-objects/flows/sign.flow'; + +describe('Snap Account Signatures and Disconnects @no-mmi', function (this: Suite) { + it('can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp({ + restrictReturnedAccounts: false, + }) + .build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver, false); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const newPublicKey = await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to experimental settings and disable redesigned signature. + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleRedesignedSignature(); + + // Open the Test Dapp and signTypedDataV3 + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage(); + await signTypedDataV3WithSnapAccount(driver, newPublicKey, false, true); + + // Disconnect from Test Dapp and reconnect to Test Dapp + await testDapp.disconnectAccount(newPublicKey); + await testDapp.connectAccount(newPublicKey); + + // SignTypedDataV4 with Test Dapp + await signTypedDataV4WithSnapAccount(driver, newPublicKey, false, true); + }, + ); + }); +}); diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts new file mode 100644 index 000000000000..fd2fe013c3c1 --- /dev/null +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -0,0 +1,101 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES, withFixtures } from '../../helpers'; +import ExperimentalSettings from '../../page-objects/pages/experimental-settings'; +import FixtureBuilder from '../../fixture-builder'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + personalSignWithSnapAccount, + signPermitWithSnapAccount, + signTypedDataV3WithSnapAccount, + signTypedDataV4WithSnapAccount, + signTypedDataWithSnapAccount, +} from '../../page-objects/flows/sign.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; + +describe('Snap Account Signatures @no-mmi', function (this: Suite) { + this.timeout(120000); // This test is very long, so we need an unusually high timeout + // Run sync, async approve, and async reject flows + // (in Jest we could do this with test.each, but that does not exist here) + + ['sync', 'approve', 'reject'].forEach((flowType) => { + // generate title of the test from flowType + const title = `can sign with ${flowType} flow`; + + it(title, async () => { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp({ + restrictReturnedAccounts: false, + }) + .build(), + title, + }, + async ({ driver }: { driver: Driver }) => { + const isSyncFlow = flowType === 'sync'; + const approveTransaction = flowType === 'approve'; + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver, isSyncFlow); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const newPublicKey = await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to experimental settings and disable redesigned signature. + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleRedesignedSignature(); + + // Run all 5 signature types + await new TestDapp(driver).openTestDappPage(); + await personalSignWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV3WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV4WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signPermitWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/account/snap-account-transfers.spec.ts b/test/e2e/tests/account/snap-account-transfers.spec.ts new file mode 100644 index 000000000000..23cc5d510eb2 --- /dev/null +++ b/test/e2e/tests/account/snap-account-transfers.spec.ts @@ -0,0 +1,165 @@ +import { Suite } from 'mocha'; +import { + multipleGanacheOptions, + PRIVATE_KEY_TWO, + WINDOW_TITLES, + withFixtures, +} from '../../helpers'; +import { DEFAULT_FIXTURE_ACCOUNT } from '../../constants'; +import { Driver } from '../../webdriver/driver'; +import { Ganache } from '../../seeder/ganache'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import FixtureBuilder from '../../fixture-builder'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +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'; +import { sendTransactionWithSnapAccount } from '../../page-objects/flows/send-transaction.flow'; + +describe('Snap Account Transfers @no-mmi', function (this: Suite) { + it('can import a private key and transfer 1 ETH (sync flow)', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: multipleGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // import snap account with private key on snap simple keyring page. + await snapSimpleKeyringPage.importAccountWithPrivateKey( + PRIVATE_KEY_TWO, + ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // send 1 ETH from snap account to account 1 + await sendTransactionWithSnapAccount( + driver, + DEFAULT_FIXTURE_ACCOUNT, + '1', + '0.000042', + '1.000042', + ); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.openAccountMenu(); + const accountList = new AccountListPage(driver); + await accountList.check_pageIsLoaded(); + + // check the balance of the 2 accounts are updated + await accountList.check_accountBalanceDisplayed('26'); + await accountList.check_accountBalanceDisplayed('24'); + }, + ); + }); + + it('can import a private key and transfer 1 ETH (async flow approve)', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: multipleGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver, false); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // import snap account with private key on snap simple keyring page. + await snapSimpleKeyringPage.importAccountWithPrivateKey( + PRIVATE_KEY_TWO, + ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // send 1 ETH from snap account to account 1 and approve the transaction + await sendTransactionWithSnapAccount( + driver, + DEFAULT_FIXTURE_ACCOUNT, + '1', + '0.000042', + '1.000042', + false, + ); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.openAccountMenu(); + const accountList = new AccountListPage(driver); + await accountList.check_pageIsLoaded(); + + // check the balance of the 2 accounts are updated + await accountList.check_accountBalanceDisplayed('26'); + await accountList.check_accountBalanceDisplayed('24'); + }, + ); + }); + + it('can import a private key and transfer 1 ETH (async flow reject)', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: multipleGanacheOptions, + title: this.test?.fullTitle(), + ignoredConsoleErrors: ['Request rejected by user or snap.'], + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + await installSnapSimpleKeyring(driver, false); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // Import snap account with private key on snap simple keyring page. + await snapSimpleKeyringPage.importAccountWithPrivateKey( + PRIVATE_KEY_TWO, + ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // send 1 ETH from snap account to account 1 and reject the transaction + await sendTransactionWithSnapAccount( + driver, + DEFAULT_FIXTURE_ACCOUNT, + '1', + '0.000042', + '1.000042', + false, + false, + ); + + // check the transaction is failed in MetaMask activity list + const homepage = new HomePage(driver); + await homepage.check_pageIsLoaded(); + await homepage.check_failedTxNumberDisplayedInActivity(); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 596c7623208d..1f4a3e5cda79 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -84,10 +84,6 @@ export class BridgePage { verifySwapPage = async (expectedHandleCount: number) => { await this.driver.delay(4000); - await this.driver.waitForSelector({ - css: '.bridge__title', - text: 'Bridge', - }); assert.equal( (await this.driver.getAllWindowHandles()).length, IS_FIREFOX || !isManifestV3 @@ -120,7 +116,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..8da5e411a2f4 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, @@ -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: '0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d730103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea491b', + }); + + await driver.waitForSelector({ + css: '#signPermitResultR', + text: 'r: 0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d73', + }); + await driver.waitForSelector({ + css: '#signPermitResultS', + text: 's: 0x0103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea49', + }); + + await driver.waitForSelector({ + css: '#signPermitResultV', + text: 'v: 27', + }); 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/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts index 7f26e02a572c..1f9d05cd26a8 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts @@ -3,7 +3,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL } from '../../../constants'; import { unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; @@ -57,7 +57,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts index 438b3e979d0a..c9c9fdbd5eda 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts @@ -2,7 +2,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL, unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); diff --git a/test/e2e/tests/confirmations/transactions/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/erc20-token-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts new file mode 100644 index 000000000000..7bacf156b71a --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts @@ -0,0 +1,195 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { DAPP_URL } from '../../../constants'; +import { + unlockWallet, + veryLargeDelayMs, + WINDOW_TITLES, +} from '../../../helpers'; +import { Mockttp } from '../../../mock-e2e'; +import WatchAssetConfirmation from '../../../page-objects/pages/confirmations/legacy/watch-asset-confirmation'; +import TokenTransferTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/token-transfer-confirmation'; +import HomePage from '../../../page-objects/pages/homepage'; +import SendTokenPage from '../../../page-objects/pages/send/send-token-page'; +import TestDapp from '../../../page-objects/pages/test-dapp'; +import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from '../helpers'; +import { TestSuiteArguments } from './shared'; + +const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); + +describe('Confirmation Redesign ERC20 Token Send @no-mmi', function () { + describe('Wallet initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + }); + + describe('dApp initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + mocks, + SMART_CONTRACTS.HST, + ); + }); + }); +}); + +async function mocks(server: Mockttp) { + return [await mockedSourcifyTokenSend(server)]; +} + +export async function mockedSourcifyTokenSend(mockServer: Mockttp) { + return await mockServer + .forGet('https://www.4byte.directory/api/v1/signatures/') + .withQuery({ hex_signature: '0xa9059cbb' }) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + count: 1, + next: null, + previous: null, + results: [ + { + bytes_signature: '©\u0005œ»', + created_at: '2016-07-09T03:58:28.234977Z', + hex_signature: '0xa9059cbb', + id: 145, + text_signature: 'transfer(address,uint256)', + }, + ], + }, + })); +} + +async function createWalletInitiatedTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.HST); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + + await testDapp.clickERC20WatchAssetButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + const watchAssetConfirmation = new WatchAssetConfirmation(driver); + await watchAssetConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + const homePage = new HomePage(driver); + await homePage.startSendFlow(); + + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.fillRecipient('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); + await sendToPage.fillAmount('1'); + + await sendToPage.click_assetPickerButton(); + await sendToPage.click_secondTokenListButton(); + await sendToPage.goToNextScreen(); + + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_walletInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_interactingWithParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} + +async function createDAppInitiatedTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.HST); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + + await testDapp.clickERC20WatchAssetButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const watchAssetConfirmation = new WatchAssetConfirmation(driver); + await watchAssetConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await testDapp.clickERC20TokenTransferButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_dappInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_interactingWithParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} diff --git a/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts index f91b1e8ba1d2..c7ceb6c42c94 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ import { MockttpServer } from 'mockttp'; -import { veryLargeDelayMs, WINDOW_TITLES } from '../../../helpers'; +import { WINDOW_TITLES } from '../../../helpers'; import { Driver } from '../../../webdriver/driver'; import { scrollAndConfirmAndAssertConfirm } from '../helpers'; import { @@ -119,8 +119,6 @@ async function createMintTransaction(driver: Driver) { } export async function confirmMintTransaction(driver: Driver) { - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ @@ -129,6 +127,12 @@ export async function confirmMintTransaction(driver: Driver) { }); await scrollAndConfirmAndAssertConfirm(driver); + + // Verify Mint Transaction is Confirmed before proceeding + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + await driver.clickElement('[data-testid="account-overview__activity-tab"]'); + await driver.waitForSelector('.transaction-status-label--confirmed'); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); } async function createApproveTransaction(driver: Driver) { @@ -137,8 +141,6 @@ async function createApproveTransaction(driver: Driver) { } async function assertApproveDetails(driver: Driver) { - await driver.delay(veryLargeDelayMs); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ @@ -191,8 +193,6 @@ async function assertApproveDetails(driver: Driver) { async function confirmApproveTransaction(driver: Driver) { await scrollAndConfirmAndAssertConfirm(driver); - - await driver.delay(veryLargeDelayMs); await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts index 5a8dcd3768f7..b0f1291a47d9 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts @@ -3,7 +3,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL } from '../../../constants'; import { unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; @@ -80,7 +80,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts index 7ca9518cabc2..9e481ee9c75f 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts @@ -2,7 +2,7 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { DAPP_URL, unlockWallet, WINDOW_TITLES } from '../../../helpers'; import { Mockttp } from '../../../mock-e2e'; -import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/set-approval-for-all-transaction-confirmation'; +import SetApprovalForAllTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/set-approval-for-all-transaction-confirmation'; import TestDapp from '../../../page-objects/pages/test-dapp'; import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; import { Driver } from '../../../webdriver/driver'; @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); 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..4b6c210f0a98 100644 --- a/test/e2e/tests/dapp-interactions/permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/permissions.spec.js @@ -28,23 +28,15 @@ describe('Permissions', function () { tag: 'button', }); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', }); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // shows connected sites await driver.clickElement( @@ -68,21 +60,17 @@ describe('Permissions', function () { assert.equal(domains.length, 1); // can get accounts within the dapp - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement({ text: 'eth_accounts', tag: 'button', }); - const getAccountsResult = await driver.waitForSelector({ + await driver.waitForSelector({ css: '#getAccountsResult', text: publicAddress, }); - assert.equal( - (await getAccountsResult.getText()).toLowerCase(), - publicAddress.toLowerCase(), - ); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/provider-api.spec.js b/test/e2e/tests/dapp-interactions/provider-api.spec.js index 80cca60afb95..1c20b9fb2f6e 100644 --- a/test/e2e/tests/dapp-interactions/provider-api.spec.js +++ b/test/e2e/tests/dapp-interactions/provider-api.spec.js @@ -1,5 +1,5 @@ const { strict: assert } = require('assert'); -const { errorCodes } = require('eth-rpc-errors'); +const { errorCodes } = require('@metamask/rpc-errors'); const { defaultGanacheOptions, withFixtures, diff --git a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js index 14d2af60bdbc..696095e3fd79 100644 --- a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, openDapp, @@ -25,12 +24,10 @@ describe('Wallet Revoke Permissions', function () { // Get initial accounts permissions await driver.clickElement('#getPermissions'); - const permissionsResult = await driver.findElement( - '#permissionsResult', - ); - - // Eth_accounts permission - assert.equal(await permissionsResult.getText(), 'eth_accounts'); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'eth_accounts', + }); // Revoke eth_accounts permissions await driver.clickElement('#revokeAccountsPermission'); @@ -39,10 +36,10 @@ describe('Wallet Revoke Permissions', function () { await driver.clickElement('#getPermissions'); // Eth_accounts permissions removed - assert.equal( - await permissionsResult.getText(), - 'No permissions found.', - ); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'No permissions found.', + }); }, ); }); 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..f96d03d96da0 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", @@ -229,11 +229,13 @@ "useTransactionSimulations": true, "enableMV3TimestampSave": true, "useExternalServices": "boolean", - "selectedAddress": "string" + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SignatureController": { + "signatureRequests": "object", "unapprovedPersonalMsgs": "object", "unapprovedTypedMessages": "object", "unapprovedPersonalMsgCount": 0, @@ -242,7 +244,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, @@ -305,8 +309,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..d7c2caead3a5 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,17 +31,21 @@ "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, "knownMethodData": "object", "use4ByteResolution": true, + "showIncomingTransactions": "object", "participateInMetaMetrics": true, "dataCollectionForMarketing": "boolean", "nextNonce": null, @@ -70,6 +73,7 @@ "showAccountBanner": true, "trezorModel": null, "onboardingDate": null, + "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, @@ -95,7 +99,9 @@ "status": "available" } }, + "networkConfigurationsByChainId": "object", "keyrings": "object", + "selectedAddress": "string", "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, @@ -118,6 +124,7 @@ "forgottenPassword": false, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", + "isMultiAccountBalancesEnabled": "boolean", "useAddressBarEnsResolution": true, "ledgerTransportType": "webhid", "snapRegistryList": "object", @@ -127,16 +134,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", @@ -167,7 +173,9 @@ "allDetectedTokens": {}, "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" }, "allNftContracts": "object", @@ -177,6 +185,7 @@ "logs": "object", "methodData": "object", "lastFetchedBlockNumbers": "object", + "submitHistory": "object", "fiatCurrency": "usd", "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, "cryptocurrencies": ["btc"], @@ -209,6 +218,7 @@ "accounts": "object", "accountsByChainId": "object", "marketData": "object", + "signatureRequests": "object", "unapprovedDecryptMsgs": "object", "unapprovedDecryptMsgCount": 0, "unapprovedEncryptionPublicKeyMsgs": "object", @@ -248,17 +258,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..89b1b29100bb 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -50,6 +50,23 @@ }, "snapsInstallPrivacyWarningShown": true }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { @@ -80,11 +97,11 @@ }, "NetworkController": { "selectedNetworkClientId": "string", + "networkConfigurations": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": "object", - "networkConfigurations": "object" + "providerConfig": "object" }, "OnboardingController": { "completedOnboarding": true, @@ -112,11 +129,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", @@ -127,14 +146,18 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, @@ -152,7 +175,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..f13d3e078c64 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -50,6 +50,23 @@ }, "snapsInstallPrivacyWarningShown": true }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { @@ -80,11 +97,11 @@ }, "NetworkController": { "selectedNetworkClientId": "string", + "networkConfigurations": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": "object", - "networkConfigurations": "object" + "providerConfig": "object" }, "OnboardingController": { "completedOnboarding": true, @@ -112,11 +129,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", @@ -127,14 +146,18 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, @@ -161,7 +184,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..6fc7025f5dbc 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -396,8 +396,12 @@ describe('MultiRpc:', function (this: Suite) { await driver.delay(regularDelayMs); // go to advanced settigns + await driver.clickElementAndWaitToDisappear({ + text: 'Manage default privacy settings', + }); + await driver.clickElement({ - text: 'Advanced configuration', + text: 'General', }); // open edit modal @@ -414,12 +418,28 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Save', tag: 'button', }); + await driver.clickElement('[data-testid="category-back-button"]'); + + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -433,8 +453,18 @@ describe('MultiRpc:', function (this: Suite) { true, '“Arbitrum One” was successfully edited!', ); + // Ensures popover backround doesn't kill test + await driver.assertElementNotPresent('.popover-bg'); + + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ tag: 'h6', text: 'Got it' }); + + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); - await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="network-display"]'); const arbitrumRpcUsed = await driver.findElement({ 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/network/update-network.spec.ts b/test/e2e/tests/network/update-network.spec.ts index 08b1bc570c83..3f0b9882688f 100644 --- a/test/e2e/tests/network/update-network.spec.ts +++ b/test/e2e/tests/network/update-network.spec.ts @@ -240,7 +240,13 @@ describe('Update Network:', function (this: Suite) { // Re-open the network menu await driver.delay(regularDelayMs); + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed await driver.clickElementSafe({ text: 'Got it', tag: 'h6' }); + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); await driver.clickElement('[data-testid="network-display"]'); // Go back to edit the network @@ -360,6 +366,13 @@ describe('Update Network:', function (this: Suite) { // Re-open the network menu await driver.delay(regularDelayMs); + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ text: 'Got it', tag: 'h6' }); + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); await driver.clickElement('[data-testid="network-display"]'); // Go back to edit the network diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 1aa716953703..de040f825ee6 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -20,7 +20,6 @@ const { onboardingCompleteWalletCreation, regularDelayMs, unlockWallet, - tinyDelayMs, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { @@ -204,7 +203,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 +269,119 @@ 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 privacy 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.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 rpcUrlInputDropDown.click(); + 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.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Save', + }); + + await driver.clickElement('[data-testid="category-back-button"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); - // Check localhost 8546 is selected and its balance value is correct - await driver.findElement({ - css: '[data-testid="network-display"]', - text: networkName, - }); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); - await locateAccountBalanceDOM(driver, secondaryGanacheServer[0]); + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement('.mm-picker-network'); + await driver.clickElement( + `[data-rbd-draggable-id="${toHex(chainId)}"]`, + ); + // 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 +396,31 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ + text: 'Manage default privacy 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"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-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', @@ -464,8 +524,6 @@ describe('MetaMask onboarding @no-mmi', function () { // pin extension walkthrough screen await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.delay(regularDelayMs); - for (let i = 0; i < mockedEndpoints.length; i += 1) { const mockedEndpoint = await mockedEndpoints[i]; const isPending = await mockedEndpoint.isPending(); 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..674ba8772e29 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -4,7 +4,6 @@ const { withFixtures, importSRPOnboardingFlow, WALLET_PASSWORD, - tinyDelayMs, defaultGanacheOptions, } = require('../../helpers'); const { METAMASK_STALELIST_URL } = require('../phishing-controller/helpers'); @@ -41,7 +40,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,21 +56,57 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ + text: 'Manage default privacy 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('[data-testid="category-back-button"]'); + await driver.clickElement('[data-testid="category-item-Assets"]'); await driver.clickElement( '[data-testid="currency-rate-check-toggle"] .toggle-button', ); - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElement('[data-testid="category-back-button"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving({ + text: 'Done', + tag: 'button', + }); + await driver.clickElementAndWaitToDisappear({ + 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'); @@ -90,7 +125,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 +141,36 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); - + await driver.clickElement({ + text: 'Manage default privacy settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="category-back-button"]', + ); + await driver.clickElement('[data-testid="category-back-button"]'); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[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('[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/smart-transactions/mock-requests-for-swap-test.ts b/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts new file mode 100644 index 000000000000..457d1ea6c0a1 --- /dev/null +++ b/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts @@ -0,0 +1,346 @@ +import { MockttpServer } from 'mockttp'; +import { mockEthDaiTrade } from '../swaps/shared'; + +const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33'; + +const GET_FEES_REQUEST_INCLUDES = { + txs: [ + { + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + value: '0x1bc16d674ec80000', + gas: '0xf4240', + nonce: '0x0', + }, + ], +}; + +const GET_FEES_RESPONSE = { + blockNumber: 20728974, + id: '19d4eea3-8a49-463e-9e9c-099f9d9571ca', + txs: [ + { + cancelFees: [], + return: '0x', + status: 1, + gasUsed: 190780, + gasLimit: 239420, + fees: [ + { + maxFeePerGas: 4667609171, + maxPriorityFeePerGas: 1000000004, + gas: 239420, + balanceNeeded: 1217518987960240, + currentBalance: 751982303082919400, + error: '', + }, + ], + feeEstimate: 627603309182220, + baseFeePerGas: 2289670348, + maxFeeEstimate: 1117518987720820, + }, + ], +}; + +const SUBMIT_TRANSACTIONS_REQUEST_EXACTLY = { + rawTxs: [ + '0x02f91a3b0180843b9aca048501163610538303a73c94881d40237659c251811cec9c364ef91dc08d300c881bc16d674ec80000b919c65f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136b796265725377617046656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000018e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000001b83413f0b3640000000000000000000000000000000000000000000000000f7a8daea356aa92bfe0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000003e2c284391c000000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017a4e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000132000000000000000000000000000000000000000000000000000000000000015200000000000000000000000000000000000000000000000000000000000001260000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000000000066e029cc00000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008200000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000e80000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000400e00deaa000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000c697051d1c6296c24ae3bcef39aca743861d9a81000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae900000000000000000000000000000000000000000000000006e0d04fc2cd90000000000000000000000000000000000000000000000000000000000000000040003c5f890000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000ead050515e10fdb3540ccd6f8236c46790508a7600000000000000000000000000000000000000000000000074b3f935bb79d45400000000000000000000000000000000000000000000000000000000000000e00000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000000000000000000000000000c4000000000000000000000000000000000000000000000000000000000000000000000000000003a4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000028cacd5e26a719f139e2105ca1efc3d9dc892826000000000000000000000000ff8ba4d1fc3762f6154cc942ccf30049a2a0cec6000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000074b3f935bb79d45400000000000000000000000000000000000000000000000074b3f935bb79d4540000000000000000000000000000000000000000000000000000000045aff3b30000000000000000000000000000000000000000000000000000000066e025760000000000000000000000000000000000000000000000000016649acb241b017da53b79cbf14cc2a737cd6469098549000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae80464000000000000000000000000ae4fdcc420f1409c8b9b2af04db150dd986f66a5000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000444b27250000000000000000000000000000000000000000000000000000000000000041031bc9026b766621ebb870691407a8f5b5d222977566d0bb38bbd633459fc9671e24b5c970373555d66f0a46e830ee1605152bd519fed1a9684a097364f8b41f1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413e4923699eff11cb0252c3f8b42793eeac8793bea92843fa4028b80ff3391bbf1df4ddef51732ceeb6f65a8c9dc2651e4b952568d350b4029d4b8b5cae5c1f991c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000045921fcd000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000840f9f95029e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000005354532a0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000840f9f95029e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040301a40330000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000005b6a0771c752e35b2ca2aff4f22a66b1598a2bc50000000000000000000000000000000000000000000000000000000053516d7f000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000005351dd8d000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000004207cfca814f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c806df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000029a7a0aa0000000000000000000000000000000000000000000000000000000000000020000000000000000000108fd5cc11eaa000000000000000fcb6c0091c62637b42000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000001b83413f0b3640000000000000000000000000000000000000000000000000f7a8daea356aa92bfe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002337b22536f75726365223a226d6574616d61736b222c22416d6f756e74496e555344223a22343635382e373832393833313733303632222c22416d6f756e744f7574555344223a22343637362e393836303733303034303236222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2234363631373438303431393032373532373538353934222c2254696d657374616d70223a313732353936353539362c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a22546363534e7837537235376b367242794f5a74344b714472344d544637356b7651527658644230724266386e395864513869634a3830385963355155595a34675a52527645337777433237352f59586a722f34625065662b4a58514b4969556b6334356a4e73556c366e6141387141774d5a48324f4a3234657932647253386c52625551444f67784b4d6979334d413164467472575241306f6d6e664873365044624b6d6f4e494c58674b45416e497a6b6d687a675043346e396d39715043337a457459737875457042772b386356426b684e7761684f56625850635854646977334870437356365555635375522f4a495342386d6a737a494b6d664b46595a716333516c5a714e6e507a50576a3648366e73587050512b6145725338334c3544554b5868364e6a70584855764748314d7a557074584169615634737354795849582f435645685a396e76564845746b2f776b6a42673d3d227d7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ac001a0e067d3acdb151721e7fdb3834cf2563d667aad9b4c18a5afb81390d6288ac2fe9fa0e74a9e40e017bcd926f3c5da4355e0926ae7e45b3b9e1bc474507220cb43', + ], + rawCancelTxs: [], +}; + +const GET_BATCH_STATUS_RESPONSE_PENDING = { + '0d506aaa-5e38-4cab-ad09-2039cb7a0f33': { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: false, + minedTx: 'not_mined', + wouldRevertMessage: null, + minedHash: '', + duplicated: false, + timedOut: false, + proxied: false, + type: 'sentinel', + }, +}; + +const GET_BATCH_STATUS_RESPONSE_SUCCESS = { + '0d506aaa-5e38-4cab-ad09-2039cb7a0f33': { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: true, + minedTx: 'success', + wouldRevertMessage: null, + minedHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + duplicated: true, + timedOut: true, + proxied: false, + type: 'sentinel', + }, +}; + +const GET_TRANSACTION_RECEIPT_RESPONSE = { + id: 2901696354742565, + jsonrpc: '2.0', + result: { + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x2', + contractAddress: null, + cumulativeGasUsed: '0xc138b1', + effectiveGasPrice: '0x1053fcd93', + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + gasUsed: '0x2e93c', + logs: [ + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000005af3107a4000', + logIndex: '0xde', + removed: false, + topics: [ + '0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000005a275669d200', + logIndex: '0xdf', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000033dd7a160e2a300', + logIndex: '0xe0', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000006a3845cef618', + logIndex: '0xe1', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x000000000000000000000000ad30f7eebd9bd5150a256f47da41d4403033cdf0', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xd82fa167727a4dc6d6f55830a2c47abbb4b3a0f8', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000033dd7a160e2a3000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000005a275669d200', + logIndex: '0xe2', + removed: false, + topics: [ + '0xb651f2787ff61b5ab14f3936f2daebdad3d84aeb74438e82870cc3b7aee71e90', + '0x00000000000000000000000000000000000000000000000000000191e0cc96ac', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000000000cbba106e00', + logIndex: '0xe3', + removed: false, + topics: [ + '0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xf326e4de8f66a0bdc0970b79e0924e33c79f1915', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000000000cbba106e00', + logIndex: '0xe4', + removed: false, + topics: [ + '0x3d0ce9bfc3ed7d6862dbb28b2dea94561fe714a1b4d019aa8af39730d1ad7c3d', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000033dd7a160e2a300', + logIndex: '0xe5', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + '0x0000000000000000000000005cfe73b6021e818b776b421b1c4db2474086a7e1', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x881d40237659c251811cec9c364ef91dc08d300c', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x', + logIndex: '0xe6', + removed: false, + topics: [ + '0xbeee1e6e7fe307ddcf84b0a16137a4430ad5e2480fc4f4a8e250ab56ccd7630d', + '0x015123c6e2552626efe611b6c48de60d080a6650860a38f237bc2b6f651f79d1', + '0x0000000000000000000000005cfe73b6021e818b776b421b1c4db2474086a7e1', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + ], + logsBloom: + '0x00000000000000001000000000000000000000000000000000000001000000000000010000000000000010000000000002000000080008000000040000000000a00000000000000000020008000000000000000000540000000004008020000010000000000000000000000000000801000000000000040000000010004010000000021000000000000000000000000000020041000100004020000000000000000000000200000000000040000000000000000000000000000000000000000000000002000400000000000000000000001002000400000000000002000000000020200000000400000000800000000000000000020200400000000000001000', + status: '0x1', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + type: '0x2', + }, +}; + +const GET_TRANSACTION_BY_HASH_RESPONSE = { + id: 2901696354742565, + jsonrpc: '2.0', + result: { + accessList: [], + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x2', + chainId: '0x539', + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + gas: '0x3a73c', + gasPrice: '0x1053fcd93', + hash: '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + input: + '0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001c616972737761704c696768743446656544796e616d696346697865640000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000191e0cc96ac0000000000000000000000000000000000000000000000000000000066e44f2c00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000033dd7a160e2a300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005a275669d200000000000000000000000000000000000000000000000000000000000000001bc1acb8a206598705baeb494a479a8af9dc3a9f9b7bd1ce9818360fd6f603cf0766e7bdc77f9f72e90dcd9157e007291adc6d3947e9b6d89ff412c5b54f9a17f1000000000000000000000000000000000000000000000000000000cbba106e00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000d7', + maxFeePerGas: '0x14bdcd619', + maxPriorityFeePerGas: '0x3b9aca04', + nonce: '0x127', + r: '0x5a5463bfe8e587ee1211be74580c74fa759f8292f37f970033df4b782f5e097d', + s: '0x50e403a70000b106e9f598b1b3f55b6ea9d2ec21d9fc67de63eb1d07df2767dd', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + transactionIndex: '0x2f', + type: '0x2', + v: '0x0', + value: '0x5af3107a4000', + yParity: '0x0', + }, +}; + +export const mockSwapRequests = async (mockServer: MockttpServer) => { + await mockEthDaiTrade(mockServer); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getBalance', + params: ['0x5cfe73b6021e818b776b421b1c4db2474086a7e1'], + }) + .thenJson(200, { + id: 3806592044086814, + jsonrpc: '2.0', + result: '0x1bc16d674ec80000', // 2 ETH + }); + + await mockServer + .forPost('https://transaction.api.cx.metamask.io/networks/1/getFees') + .withJsonBodyIncluding(GET_FEES_REQUEST_INCLUDES) + .thenJson(200, GET_FEES_RESPONSE); + + await mockServer + .forPost( + 'https://transaction.api.cx.metamask.io/networks/1/submitTransactions', + ) + .once() + .withJsonBody(SUBMIT_TRANSACTIONS_REQUEST_EXACTLY) + .thenJson(200, { uuid: STX_UUID }); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, GET_BATCH_STATUS_RESPONSE_PENDING); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, GET_BATCH_STATUS_RESPONSE_SUCCESS); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getTransactionReceipt', + params: [ + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + ], + }) + .thenJson(200, GET_TRANSACTION_RECEIPT_RESPONSE); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getTransactionByHash', + params: [ + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + ], + }) + .thenJson(200, GET_TRANSACTION_BY_HASH_RESPONSE); +}; diff --git a/test/e2e/tests/smart-transactions/smart-transactions.spec.ts b/test/e2e/tests/smart-transactions/smart-transactions.spec.ts new file mode 100644 index 000000000000..210d5abdb034 --- /dev/null +++ b/test/e2e/tests/smart-transactions/smart-transactions.spec.ts @@ -0,0 +1,97 @@ +import { MockttpServer } from 'mockttp'; +import { + buildQuote, + reviewQuote, + checkActivityTransaction, +} from '../swaps/shared'; +import FixtureBuilder from '../../fixture-builder'; +import { unlockWallet, withFixtures } from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { mockSwapRequests } from './mock-requests-for-swap-test'; + +export async function withFixturesForSmartTransactions( + { + title, + testSpecificMock, + }: { + title?: string; + testSpecificMock: (mockServer: MockttpServer) => Promise; + }, + test: (args: { driver: Driver }) => Promise, +) { + const inputChainId = CHAIN_IDS.MAINNET; + await withFixtures( + { + fixtures: new FixtureBuilder({ inputChainId }) + .withPermissionControllerConnectedToTestDapp() + .withPreferencesControllerSmartTransactionsOptedIn() + .withNetworkControllerOnMainnet() + .build(), + title, + testSpecificMock, + dapp: true, + }, + async ({ driver }) => { + await unlockWallet(driver); + await test({ driver }); + }, + ); +} + +export const waitForTransactionToComplete = async ( + driver: Driver, + options: { tokenName: string }, +) => { + await driver.waitForSelector({ + css: '[data-testid="swap-smart-transaction-status-header"]', + text: 'Privately submitting your Swap', + }); + + await driver.waitForSelector( + { + css: '[data-testid="swap-smart-transaction-status-header"]', + text: 'Swap complete!', + }, + { timeout: 30000 }, + ); + + await driver.findElement({ + css: '[data-testid="swap-smart-transaction-status-description"]', + text: `${options.tokenName}`, + }); + + await driver.clickElement({ text: 'Close', tag: 'button' }); + await driver.waitForSelector('[data-testid="account-overview__asset-tab"]'); +}; + +describe('smart transactions @no-mmi', function () { + it('Completes a Swap', async function () { + await withFixturesForSmartTransactions( + { + title: this.test?.fullTitle(), + testSpecificMock: mockSwapRequests, + }, + async ({ driver }) => { + await buildQuote(driver, { + amount: 2, + swapTo: 'DAI', + }); + await reviewQuote(driver, { + amount: 2, + swapFrom: 'ETH', + swapTo: 'DAI', + }); + + await driver.clickElement({ text: 'Swap', tag: 'button' }); + await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); + await checkActivityTransaction(driver, { + index: 0, + amount: '2', + swapFrom: 'ETH', + swapTo: 'DAI', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/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/swaps/shared.js b/test/e2e/tests/swaps/shared.ts similarity index 71% rename from test/e2e/tests/swaps/shared.js rename to test/e2e/tests/swaps/shared.ts index 3bfdefcf71d7..3f3aff4447e5 100644 --- a/test/e2e/tests/swaps/shared.js +++ b/test/e2e/tests/swaps/shared.ts @@ -1,27 +1,54 @@ -const { strict: assert } = require('assert'); -const FixtureBuilder = require('../../fixture-builder'); -const { regularDelayMs, veryLargeDelayMs } = require('../../helpers'); - -const ganacheOptions = { - accounts: [ - { - secretKey: - '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', - balance: 25000000000000000000, +import { strict as assert } from 'assert'; +import { ServerOptions } from 'ganache'; +import { MockttpServer } from 'mockttp'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { regularDelayMs, veryLargeDelayMs } from '../../helpers'; +import { SWAP_TEST_ETH_DAI_TRADES_MOCK } from '../../../data/mock-data'; + +export async function mockEthDaiTrade(mockServer: MockttpServer) { + return [ + await mockServer + .forGet('https://swap.api.cx.metamask.io/networks/1/trades') + .thenCallback(() => { + return { + statusCode: 200, + json: SWAP_TEST_ETH_DAI_TRADES_MOCK, + }; + }), + ]; +} + +export const ganacheOptions: ServerOptions & { miner: { blockTime?: number } } = + { + wallet: { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000n, + }, + ], }, - ], -}; + miner: {}, + }; -const withFixturesOptions = { +export const withFixturesOptions = { fixtures: new FixtureBuilder().build(), ganacheOptions, }; -const buildQuote = async (driver, options) => { +type SwapOptions = { + amount: number; + swapTo?: string; + swapToContractAddress?: string; +}; + +export const buildQuote = async (driver: Driver, options: SwapOptions) => { await driver.clickElement('[data-testid="token-overview-button-swap"]'); await driver.fill( 'input[data-testid="prepare-swap-page-from-token-amount"]', - options.amount, + options.amount.toString(), ); await driver.delay(veryLargeDelayMs); // Need an extra delay after typing an amount. await driver.clickElement('[data-testid="prepare-swap-page-swap-to"]'); @@ -29,7 +56,7 @@ const buildQuote = async (driver, options) => { await driver.fill( 'input[id="list-with-search__text-search"]', - options.swapTo || options.swapToContractAddress, + options.swapTo || options.swapToContractAddress || '', ); await driver.delay(veryLargeDelayMs); // Need an extra delay after typing an amount. @@ -55,7 +82,15 @@ const buildQuote = async (driver, options) => { ); }; -const reviewQuote = async (driver, options) => { +export const reviewQuote = async ( + driver: Driver, + options: { + swapFrom: string; + swapTo: string; + amount: number; + skipCounter?: boolean; + }, +) => { const summary = await driver.waitForSelector( '[data-testid="exchange-rate-display-quote-rate"]', ); @@ -68,7 +103,7 @@ const reviewQuote = async (driver, options) => { '[data-testid="prepare-swap-page-receive-amount"]', ); const swapToAmount = await elementSwapToAmount.getText(); - const expectedAmount = parseFloat(quote[3]) * options.amount; + const expectedAmount = Number(quote[3]) * options.amount; const dotIndex = swapToAmount.indexOf('.'); const decimals = dotIndex === -1 ? 0 : swapToAmount.length - dotIndex - 1; assert.equal( @@ -91,7 +126,10 @@ const reviewQuote = async (driver, options) => { } }; -const waitForTransactionToComplete = async (driver, options) => { +export const waitForTransactionToComplete = async ( + driver: Driver, + options: { tokenName: string }, +) => { await driver.waitForSelector({ css: '[data-testid="awaiting-swap-header"]', text: 'Processing', @@ -114,7 +152,10 @@ const waitForTransactionToComplete = async (driver, options) => { await driver.waitForSelector('[data-testid="account-overview__asset-tab"]'); }; -const checkActivityTransaction = async (driver, options) => { +export const checkActivityTransaction = async ( + driver: Driver, + options: { index: number; swapFrom: string; swapTo: string; amount: string }, +) => { await driver.clickElement('[data-testid="account-overview__activity-tab"]'); await driver.waitForSelector('.activity-list-item'); @@ -149,7 +190,10 @@ const checkActivityTransaction = async (driver, options) => { await driver.clickElement('[data-testid="popover-close"]'); }; -const checkNotification = async (driver, options) => { +export const checkNotification = async ( + driver: Driver, + options: { title: string; text: string }, +) => { const isExpectedBoxTitlePresentAndVisible = await driver.isElementPresentAndVisible({ css: '[data-testid="swaps-banner-title"]', @@ -171,7 +215,7 @@ const checkNotification = async (driver, options) => { ); }; -const changeExchangeRate = async (driver) => { +export const changeExchangeRate = async (driver: Driver) => { await driver.clickElement('[data-testid="review-quote-view-all-quotes"]'); await driver.waitForSelector({ text: 'Quote details', tag: 'h2' }); @@ -182,13 +226,3 @@ const changeExchangeRate = async (driver) => { await networkFees[random].click(); await driver.clickElement({ text: 'Select', tag: 'button' }); }; - -module.exports = { - withFixturesOptions, - buildQuote, - reviewQuote, - waitForTransactionToComplete, - checkActivityTransaction, - checkNotification, - changeExchangeRate, -}; diff --git a/test/e2e/tests/swaps/swap-eth.spec.js b/test/e2e/tests/swaps/swap-eth.spec.ts similarity index 80% rename from test/e2e/tests/swaps/swap-eth.spec.js rename to test/e2e/tests/swaps/swap-eth.spec.ts index 35847b0ae33a..18d049e5de16 100644 --- a/test/e2e/tests/swaps/swap-eth.spec.js +++ b/test/e2e/tests/swaps/swap-eth.spec.ts @@ -1,35 +1,22 @@ -const { withFixtures, unlockWallet } = require('../../helpers'); -const { SWAP_TEST_ETH_DAI_TRADES_MOCK } = require('../../../data/mock-data'); -const { +import { unlockWallet, withFixtures } from '../../helpers'; +import { withFixturesOptions, buildQuote, reviewQuote, waitForTransactionToComplete, checkActivityTransaction, changeExchangeRate, -} = require('./shared'); - -async function mockEthDaiTrade(mockServer) { - return [ - await mockServer - .forGet('https://swap.api.cx.metamask.io/networks/1/trades') - .thenCallback(() => { - return { - statusCode: 200, - json: SWAP_TEST_ETH_DAI_TRADES_MOCK, - }; - }), - ]; -} + mockEthDaiTrade, +} from './shared'; describe('Swap Eth for another Token @no-mmi', function () { it('Completes second Swaps while first swap is processing', async function () { - withFixturesOptions.ganacheOptions.blockTime = 10; + withFixturesOptions.ganacheOptions.miner.blockTime = 10; await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -70,12 +57,13 @@ describe('Swap Eth for another Token @no-mmi', function () { }, ); }); + it('Completes a Swap between ETH and DAI after changing initial rate', async function () { await withFixtures( { ...withFixturesOptions, testSpecificMock: mockEthDaiTrade, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); diff --git a/test/e2e/tests/swaps/swaps-notifications.spec.js b/test/e2e/tests/swaps/swaps-notifications.spec.ts similarity index 92% rename from test/e2e/tests/swaps/swaps-notifications.spec.js rename to test/e2e/tests/swaps/swaps-notifications.spec.ts index c6dbbd469959..134741d3683c 100644 --- a/test/e2e/tests/swaps/swaps-notifications.spec.js +++ b/test/e2e/tests/swaps/swaps-notifications.spec.ts @@ -1,13 +1,14 @@ -const { withFixtures, unlockWallet } = require('../../helpers'); -const { SWAP_TEST_ETH_USDC_TRADES_MOCK } = require('../../../data/mock-data'); -const { +import { Mockttp } from 'mockttp'; +import { withFixtures, unlockWallet } from '../../helpers'; +import { SWAP_TEST_ETH_USDC_TRADES_MOCK } from '../../../data/mock-data'; +import { withFixturesOptions, buildQuote, reviewQuote, checkNotification, -} = require('./shared'); +} from './shared'; -async function mockSwapsTransactionQuote(mockServer) { +async function mockSwapsTransactionQuote(mockServer: Mockttp) { return [ await mockServer .forGet('https://swap.api.cx.metamask.io/networks/1/trades') @@ -19,7 +20,7 @@ async function mockSwapsTransactionQuote(mockServer) { } describe('Swaps - notifications @no-mmi', function () { - async function mockTradesApiPriceSlippageError(mockServer) { + async function mockTradesApiPriceSlippageError(mockServer: Mockttp) { await mockServer .forGet('https://swap.api.cx.metamask.io/networks/1/trades') .thenCallback(() => { @@ -71,7 +72,7 @@ describe('Swaps - notifications @no-mmi', function () { { ...withFixturesOptions, testSpecificMock: mockTradesApiPriceSlippageError, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -122,7 +123,7 @@ describe('Swaps - notifications @no-mmi', function () { ...withFixturesOptions, ganacheOptions: lowBalanceGanacheOptions, testSpecificMock: mockSwapsTransactionQuote, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -152,7 +153,7 @@ describe('Swaps - notifications @no-mmi', function () { await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -174,12 +175,12 @@ describe('Swaps - notifications @no-mmi', function () { await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); await buildQuote(driver, { - amount: '.0001', + amount: 0.0001, swapTo: 'DAI', }); await driver.clickElement('[title="Transaction settings"]'); diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index a13ef9caa2b5..5eb60d3db17b 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -8,6 +8,7 @@ const { clickNestedButton, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); const { CHAIN_IDS } = require('../../../../shared/constants/network'); describe('Add hide token', function () { @@ -119,23 +120,23 @@ 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', tag: 'p', }); await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement( + await driver.clickElementAndWaitToDisappear( '[data-testid="import-tokens-modal-import-button"]', ); await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); - const [, tkn] = await driver.findElements( - '[data-testid="multichain-token-list-button"]', - ); - await tkn.click(); + await driver.clickElement({ + tag: 'span', + text: 'Basic Attention Token', + }); await driver.waitForSelector({ css: '[data-testid="multichain-token-list-item-value"]', @@ -147,6 +148,8 @@ describe('Add existing token using search', function () { }); describe('Add token using wallet_watchAsset', function () { + const smartContract = SMART_CONTRACTS.HST; + it('opens a notification that adds a token when wallet_watchAsset is executed, then approves', async function () { await withFixtures( { @@ -155,9 +158,13 @@ describe('Add token using wallet_watchAsset', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, + smartContract, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, contractRegistry }) => { + const contractAddress = await contractRegistry.getContractAddress( + smartContract, + ); await unlockWallet(driver); await driver.openNewPage('http://127.0.0.1:8080/'); @@ -168,7 +175,7 @@ describe('Add token using wallet_watchAsset', function () { params: { type: 'ERC20', options: { - address: '0x86002be4cdd922de1ccb831582bf99284b99ac12', + address: '${contractAddress}', symbol: 'TST', decimals: 4 }, @@ -176,19 +183,16 @@ describe('Add token using wallet_watchAsset', function () { }) `); - const windowHandles = 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: 'Add token', }); - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.waitForSelector({ css: '[data-testid="multichain-token-list-item-value"]', @@ -206,9 +210,13 @@ describe('Add token using wallet_watchAsset', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, + smartContract, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, contractRegistry }) => { + const contractAddress = await contractRegistry.getContractAddress( + smartContract, + ); await unlockWallet(driver); await driver.openNewPage('http://127.0.0.1:8080/'); @@ -219,7 +227,7 @@ describe('Add token using wallet_watchAsset', function () { params: { type: 'ERC20', options: { - address: '0x86002be4cdd922de1ccb831582bf99284b99ac12', + address: '${contractAddress}', symbol: 'TST', decimals: 4 }, @@ -227,19 +235,16 @@ describe('Add token using wallet_watchAsset', function () { }) `); - const windowHandles = await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ tag: 'button', text: 'Cancel', }); - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); const assetListItems = await driver.findElements( '.multichain-token-list-item', 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/nft/erc1155-interaction.spec.js b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js index 31425140c7f4..c635d465353a 100644 --- a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js @@ -1,4 +1,5 @@ const { strict: assert } = require('assert'); +const { mockNetworkStateOld } = require('../../../../stub/networks'); const { withFixtures, DAPP_URL, @@ -19,6 +20,15 @@ describe('ERC1155 NFTs testdapp interaction', function () { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() + .withNetworkController( + mockNetworkStateOld({ + chainId: '0x539', + nickname: 'Localhost 8545', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io/', + }), + ) .build(), ganacheOptions: defaultGanacheOptions, smartContract, @@ -38,33 +48,57 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#batchMintButton'); // Notification - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Mint await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, + await driver.clickElement('[data-testid="activity-list-item-action"]'); + await driver.clickElement({ + text: 'View on block explorer', + tag: 'a', + }); + + // Switch to block explorer + await driver.switchToWindowWithTitle('E2E Test Page'); + await driver.findElement('[data-testid="empty-page-body"]'); + // Verify block explorer + await driver.waitForUrl({ + url: 'https://etherscan.io/tx/0xfe4428397f7913875783c5c0dad182937b596148295bc33c7f08d74fdee8897f', + }); + + // switch to Dapp + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.fill('#watchAssetInput', '1'); + await driver.clickElement('#watchAssetButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + await driver.clickElementSafe('[data-testid="popover-close"]'); + await driver.clickElement('[data-testid="account-overview__nfts-tab"]'); + await driver.clickElement('[data-testid="nft-item"]'); }, ); }); @@ -90,33 +124,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.fill('#batchTransferTokenAmounts', '1, 1, 1000000000000'); await driver.clickElement('#batchTransferFromButton'); - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Transfer await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -147,26 +175,20 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#setApprovalForAllERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - const displayedMessageTitle = await driver.findElement( - '[data-testid="confirm-approve-title"]', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector({ + css: '[data-testid="confirm-approve-title"]', + text: expectedMessageTitle, + }); + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -185,27 +207,29 @@ describe('ERC1155 NFTs testdapp interaction', function () { '.set-approval-for-all-warning__content__header', ); assert.equal(await displayedWarning.getText(), expectedWarningMessage); - await driver.clickElement({ text: 'Approve', tag: 'button' }); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Approve', + tag: 'button', + }); // Switch to extension and check set approval for all transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const setApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await setApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that set approval for all action completed message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - const setApprovalStatus = await driver.findElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.waitForSelector({ css: '#erc1155Status', text: 'Set Approval For All completed', }); - assert.equal(await setApprovalStatus.isDisplayed(), true); }, ); }); @@ -235,27 +259,22 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#revokeERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const displayedMessageTitle = await driver.findElement( - '.confirm-approve-content__title', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.waitForSelector({ + css: '.confirm-approve-content__title', + text: expectedMessageTitle, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -269,22 +288,25 @@ describe('ERC1155 NFTs testdapp interaction', function () { assert.equal(await params.getText(), 'Parameters: false'); // Click on extension popup to confirm revoke approval for all - await driver.clickElement('[data-testid="page-container-footer-next"]'); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); // Switch to extension and check revoke approval transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const revokeApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await revokeApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that revoke approval for all message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const revokeApprovalStatus = await driver.findElement({ css: '#erc1155Status', text: 'Revoke completed', diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index c2d3b7a8ce82..35750bae6d2c 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -51,19 +51,17 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const nftsMintStatus = await driver.findElement({ + await driver.waitForSelector({ css: '#nftsStatus', text: 'Mint completed', }); - assert.equal(await nftsMintStatus.isDisplayed(), true); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, @@ -116,11 +114,10 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -138,7 +135,6 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.fill('#watchNFTInput', '3'); await driver.clickElement({ text: 'Watch NFT', tag: 'button' }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // avoid race condition @@ -225,37 +221,40 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement({ text: 'Mint', tag: 'button' }); // Notification - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + + // We need to wait until the transaction is confirmed before looking for the tx + // otherwise the element becomes stale, as it updates from 'pending' to 'confirmed' + await driver.waitForSelector('.transaction-status-label--confirmed'); + + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const nftsMintStatus = await driver.findElement({ + await driver.waitForSelector({ css: '#nftsStatus', text: 'Mint completed', }); - assert.equal(await nftsMintStatus.isDisplayed(), true); // watch all nfts await driver.clickElement({ text: 'Watch all NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirm watchNFT @@ -277,8 +276,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); await removeButtons[0].click(); - await driver.clickElement({ text: 'Add NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Add NFTs', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, @@ -316,7 +317,6 @@ describe('ERC721 NFTs testdapp interaction', function () { // Click Transfer await driver.fill('#transferTokenInput', '1'); await driver.clickElement('#transferFromButton'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm transfer @@ -401,11 +401,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Approve TDN spending cap', }); - assert.equal(await completedTx.isDisplayed(), true); }, ); }); @@ -468,11 +467,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Approve TDN with no spend limit', }); - assert.equal(await completedTx.isDisplayed(), true); }, ); }); @@ -538,11 +536,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Approve TDN with no spend limit', }); - assert.equal(await completedTx.isDisplayed(), true); }, ); }); 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/tests/transaction/simple-send.spec.ts b/test/e2e/tests/transaction/simple-send.spec.ts index 43b096bf4ec8..0615a0e21d74 100644 --- a/test/e2e/tests/transaction/simple-send.spec.ts +++ b/test/e2e/tests/transaction/simple-send.spec.ts @@ -31,6 +31,7 @@ describe('Simple send eth', function (this: Suite) { '1.000042', ); const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); await homePage.check_confirmedTxNumberDisplayedInActivity(); await homePage.check_txAmountInActivity(); }, diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 97c616d01f4b..813d00d5e0e8 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`); } @@ -317,10 +319,14 @@ class Driver { * Waits for an element that matches the given locator to reach the specified state within the timeout period. * * @param {string | object} rawLocator - Element locator - * @param {number} timeout - optional parameter that specifies the maximum amount of time (in milliseconds) + * @param {object} [options] - parameter object + * @param {number} [options.timeout] - specifies the maximum amount of time (in milliseconds) * to wait for the condition to be met and desired state of the element to wait for. * It defaults to 'visible', indicating that the method will wait until the element is visible on the page. * The other supported state is 'detached', which means waiting until the element is removed from the DOM. + * @param {string} [options.state] - specifies the state of the element to wait for. + * It defaults to 'visible', indicating that the method will wait until the element is visible on the page. + * The other supported state is 'detached', which means waiting until the element is removed from the DOM. * @returns {Promise} promise resolving when the element meets the state or timeout occurs. * @throws {Error} Will throw an error if the element does not reach the specified state within the timeout period. */ @@ -591,6 +597,46 @@ class Driver { } } + /** + * Checks if an element is moving by comparing its position at two different times. + * + * @param {string | object} rawLocator - Element locator. + * @returns {Promise} Promise that resolves to a boolean indicating if the element is moving. + */ + async isElementMoving(rawLocator) { + const element = await this.findElement(rawLocator); + const initialPosition = await element.getRect(); + + await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for a short period + + const newPosition = await element.getRect(); + + return ( + initialPosition.x !== newPosition.x || initialPosition.y !== newPosition.y + ); + } + + /** + * Waits until an element stops moving within a specified timeout period. + * + * @param {string | object} rawLocator - Element locator. + * @param {number} timeout - The maximum time to wait for the element to stop moving. + * @returns {Promise} Promise that resolves when the element stops moving. + * @throws {Error} Throws an error if the element does not stop moving within the timeout period. + */ + async waitForElementToStopMoving(rawLocator, timeout = 5000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + if (!(await this.isElementMoving(rawLocator))) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 500)); // Check every 500ms + } + + throw new Error('Element did not stop moving within the timeout period'); + } + /** @param {string} title - The title of the window or tab the screenshot is being taken in */ async takeScreenshot(title) { const filepathBase = `${artifactDir(title)}/test-screenshot`; @@ -1311,7 +1357,10 @@ function collectMetrics() { }); }); - return results; + return { + ...results, + ...window.stateHooks.getCustomTraces(), + }; } module.exports = { Driver, PAGES }; 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..8e9c979562f2 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', () => ({ @@ -73,7 +73,7 @@ describe('Permit Confirmation', () => { jest.resetAllMocks(); mockedBackgroundConnection.submitRequestToBackground.mockImplementation( createMockImplementation({ - getTokenStandardAndDetails: { decimals: '2' }, + getTokenStandardAndDetails: { decimals: '2', standard: 'ERC20' }, }), ); }); @@ -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..c2625e06e3e7 --- /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 SepoliaETH'); + 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 SepoliaETH'); + 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..b77e48f1d660 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,18 +286,22 @@ 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'); + expect(firstGasField).toHaveTextContent('0.0001 SepoliaETH'); const editGasFeeNativeCurrency = within(editGasFeesRow).getByTestId('native-currency'); expect(editGasFeeNativeCurrency).toHaveTextContent('$0.47'); @@ -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('0.0023 ETH'); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); + expect(maxFee).toHaveTextContent('0.0023 SepoliaETH'); 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..2d9e50002a18 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 }, @@ -1210,10 +1209,6 @@ "extension_active": true, "mobile_active": true }, - "swapRedesign": { - "extensionActive": true, - "mobileActive": false - }, "zksync": { "extensionActive": true, "extension_active": true, 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..55ffa7f9ba1b 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, @@ -397,10 +397,6 @@ export const createSwapsMockStore = () => { mobileActive: true, extensionActive: true, }, - swapRedesign: { - mobileActive: true, - extensionActive: true, - }, }, quotes: { TEST_AGG_1: { @@ -469,6 +465,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 +516,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 +701,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 +726,8 @@ export const createBridgeMockStore = ( destNetworkAllowlist: [], ...featureFlagOverrides, }, + ...bridgeStateOverrides, }, - ...mockNetworkState( - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.LINEA_MAINNET }, - ), }, }; }; diff --git a/test/jest/mocks.ts b/test/jest/mocks.ts index ed89b487e3ab..be1120429290 100644 --- a/test/jest/mocks.ts +++ b/test/jest/mocks.ts @@ -180,12 +180,14 @@ export function createMockInternalAccount({ address = MOCK_DEFAULT_ADDRESS, type = EthAccountType.Eoa, keyringType = KeyringTypes.hd, + lastSelected = 0, snapOptions = undefined, }: { name?: string; address?: string; type?: string; keyringType?: string; + lastSelected?: number; snapOptions?: { enabled: boolean; name: string; @@ -228,6 +230,7 @@ export function createMockInternalAccount({ type: keyringType, }, snap: snapOptions, + lastSelected, }, options: {}, methods, diff --git a/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/test/stub/provider.js b/test/stub/provider.js index e070d55fa6b0..f86762218adf 100644 --- a/test/stub/provider.js +++ b/test/stub/provider.js @@ -1,4 +1,7 @@ -import { JsonRpcEngine, createScaffoldMiddleware } from 'json-rpc-engine'; +import { + JsonRpcEngine, + createScaffoldMiddleware, +} from '@metamask/json-rpc-engine'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; import Ganache from 'ganache'; diff --git a/types/global.d.ts b/types/global.d.ts index 95fb6c98547a..8078a3998bde 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -239,6 +239,7 @@ type HttpProvider = { }; type StateHooks = { + getCustomTraces?: () => { [name: string]: number }; getCleanAppState?: () => Promise; getLogs?: () => any[]; getMostRecentPersistedState?: () => any; 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..0995f4b52a4a 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -53,8 +53,9 @@ @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'; @import 'transaction-breakdown/index'; @import 'transaction-icon/transaction-icon'; 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/__snapshots__/nft-details.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap index 22c5342d2026..d6b0de0c043e 100644 --- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap +++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-details.test.js.snap @@ -182,183 +182,3 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = ` `; - -exports[`NFT Details should match minimal props and state snapshot 2`] = ` -
-
-
-
-
- -
- -
-
-
-
- -
-
-
-
-

- MUNK #1 -

-
-
-

-

-
-
-

- Contract address -

-
- - -
-
-
-

- Token ID -

-

- 1 -

-
-
-

- Token standard -

-

- ERC721 -

-
-
- -
-
-
- Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted. -
-
-
-
-
-
-
-`; - -exports[`NFT Details should match minimal props and state snapshot 3`] = `
`; diff --git a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap index 086754f9b489..7b4d6b11abc6 100644 --- a/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap +++ b/ui/components/app/assets/nfts/nft-details/__snapshots__/nft-full-image.test.js.snap @@ -80,5 +80,3 @@ exports[`NFT full image should match snapshot 1`] = `
`; - -exports[`NFT full image should match snapshot 2`] = `
`; 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-display/index.js b/ui/components/app/assets/token-list-display/index.js deleted file mode 100644 index 54b9cb4877d5..000000000000 --- a/ui/components/app/assets/token-list-display/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './token-list-display'; diff --git a/ui/components/app/assets/token-list-display/token-list-display.js b/ui/components/app/assets/token-list-display/token-list-display.js deleted file mode 100644 index a18f0a8f79f1..000000000000 --- a/ui/components/app/assets/token-list-display/token-list-display.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; -import { isEqual } from 'lodash'; - -import { getShouldHideZeroBalanceTokens } from '../../../../selectors'; -import { useTokenTracker } from '../../../../hooks/useTokenTracker'; -import Identicon from '../../../ui/identicon'; -import TokenBalance from '../../../ui/token-balance'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { getTokens } from '../../../../ducks/metamask/metamask'; - -export default function TokenListDisplay({ clickHandler }) { - const t = useI18nContext(); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); - - const tokens = useSelector(getTokens, isEqual); - const { loading, tokensWithBalances } = useTokenTracker({ - tokens, - includeFailedTokens: true, - hideZeroBalanceTokens: shouldHideZeroBalanceTokens, - }); - if (loading) { - return

{t('loadingTokens')}
; - } - - const sendableTokens = tokensWithBalances.filter((token) => !token.isERC721); - - return ( - <> - {sendableTokens.map((tokenData) => { - const { address, symbol, image } = tokenData; - - return ( -
clickHandler(tokenData)} - > - -
-
{symbol}
-
- - {`${t('balance')}:`} - - -
-
-
- ); - })} - - ); -} - -TokenListDisplay.propTypes = { - clickHandler: PropTypes.func, -}; diff --git a/ui/components/app/assets/token-list-display/token-list-display.scss b/ui/components/app/assets/token-list-display/token-list-display.scss deleted file mode 100644 index 6f61f9b685a1..000000000000 --- a/ui/components/app/assets/token-list-display/token-list-display.scss +++ /dev/null @@ -1,46 +0,0 @@ -@use "design-system"; - -.loading-span { - display: flex; - height: 250px; - align-items: center; - justify-content: center; - padding: 32px; -} - -.token-list-item { - display: flex; - flex-flow: row nowrap; - align-items: center; - cursor: pointer; - padding: 8px; - - &:hover { - background-color: var(--color-background-hover); - } - - &__data { - margin-left: 8px; - } - - &__symbol { - @include design-system.Paragraph; - - line-height: 140%; - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__balance { - @include design-system.H7; - - display: flex; - flex-flow: row nowrap; - } - - &__balance__label { - margin-right: 4px; - } -} 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/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index 14abe8eceea7..a41b92c9f463 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -43,6 +43,7 @@ import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; import { SnapUICard } from '../snaps/snap-ui-card'; import { SnapUIAddress } from '../snaps/snap-ui-address'; +import { SnapUIAvatar } from '../snaps/snap-ui-avatar'; import { SnapUISelector } from '../snaps/snap-ui-selector'; import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -106,6 +107,7 @@ export const safeComponentList = { SnapUICard, SnapUISelector, SnapUIAddress, + SnapUIAvatar, SnapUIFooterButton, FormTextField, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) 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/qr-hardware-popover/qr-hardware-popover.js b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js index 40d577380f49..ad0dff033ae9 100644 --- a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js +++ b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { getCurrentQRHardwareState } from '../../../selectors'; import Popover from '../../ui/popover'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -44,7 +44,7 @@ const QRHardwarePopover = () => { dispatch( rejectPendingApproval( _txData.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); dispatch(cancelTx(_txData)); 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`] = ` + +
+
+
+ @@ -409,7 +409,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -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..7921af85f2ce 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -6,7 +6,7 @@ import { I18nContext } from '../../../contexts/i18n'; import { SEND_ROUTE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, ///: END:ONLY_INCLUDE_IF } from '../../../helpers/constants/routes'; import { startNewDraftTransaction } from '../../../ducks/send'; @@ -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={() => { @@ -260,12 +276,12 @@ const TokenButtons = ({ ); if (usingHardwareWallet) { global.platform.openExtensionInBrowser?.( - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, undefined, false, ); } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } ///: END:ONLY_INCLUDE_IF }} @@ -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/__snapshots__/index.test.tsx.snap b/ui/pages/bridge/__snapshots__/index.test.tsx.snap index 14514e59987d..cebca14e93bb 100644 --- a/ui/pages/bridge/__snapshots__/index.test.tsx.snap +++ b/ui/pages/bridge/__snapshots__/index.test.tsx.snap @@ -9,27 +9,61 @@ exports[`Bridge renders the component with initial props 1`] = ` class="bridge__container" >

- +
- Bridge +

+ Bridge +

+
+
+
+
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.scss b/ui/pages/bridge/index.scss index 99b8712cc63e..98a3a3ee5c34 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -1 +1,29 @@ @use "design-system"; + +@import 'prepare/index'; + +.bridge { + max-height: 100vh; + width: 360px; + position: relative; + + &__container { + width: 100%; + + .multichain-page-footer { + position: absolute; + width: 100%; + height: 80px; + bottom: 0; + padding: 16px; + display: flex; + + button { + flex: 1; + height: 100%; + font-size: 14px; + font-weight: 500; + } + } + } +} diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 4352ff359742..0d0d4c21c71f 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); @@ -62,8 +64,6 @@ describe('Bridge', () => { it('renders the component with initial props', async () => { const swapsMockStore = createBridgeMockStore({ extensionSupport: true }); - swapsMockStore.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = - true; const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider( diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index f216a52ec71d..e81b20670011 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -1,9 +1,7 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Switch, useHistory } from 'react-router-dom'; -import classnames from 'classnames'; import { I18nContext } from '../../contexts/i18n'; - import { clearSwapsState } from '../../ducks/swaps/swaps'; import { DEFAULT_ROUTE, @@ -11,36 +9,51 @@ import { PREPARE_SWAP_ROUTE, CROSS_CHAIN_SWAP_ROUTE, } from '../../helpers/constants/routes'; - import { resetBackgroundSwapsState } from '../../store/actions'; - import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route'; import { - Box, - Icon, + ButtonIcon, + ButtonIconSize, IconName, - IconSize, } from '../../components/component-library'; +import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors'; +import useBridging from '../../hooks/bridge/useBridging'; import { - JustifyContent, - IconColor, - Display, - BlockSize, -} from '../../helpers/constants/design-system'; -import { getIsBridgeEnabled } from '../../selectors'; -import { PrepareBridgePage } from './prepare/prepare-bridge-page'; + Content, + Footer, + Header, +} from '../../components/multichain/pages/page'; +import { getProviderConfig } from '../../ducks/metamask/metamask'; +import { resetInputFields, setFromChain } from '../../ducks/bridge/actions'; +import PrepareBridgePage from './prepare/prepare-bridge-page'; +import { BridgeCTAButton } from './prepare/bridge-cta-button'; const CrossChainSwap = () => { const t = useContext(I18nContext); + + useBridging(); + const history = useHistory(); const dispatch = useDispatch(); const isBridgeEnabled = useSelector(getIsBridgeEnabled); + const providerConfig = useSelector(getProviderConfig); + const isBridgeChain = useSelector(getIsBridgeChain); + + useEffect(() => { + isBridgeChain && + isBridgeEnabled && + providerConfig && + dispatch(setFromChain(providerConfig.chainId)); + + return () => { + dispatch(resetInputFields()); + }; + }, [isBridgeChain, isBridgeEnabled, providerConfig]); const redirectToDefaultRoute = async () => { history.push({ pathname: DEFAULT_ROUTE, - // @ts-expect-error - property 'state' does not exist on type PartialPath. state: { stayOnHomePage: true }, }); dispatch(clearSwapsState()); @@ -50,37 +63,27 @@ const CrossChainSwap = () => { return (
-
- ) => { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - - - -
{t('bridge')}
-
-
+ } > + {t('bridge')} + + { }} /> -
+ +
+ +
); diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap new file mode 100644 index 000000000000..f225adec3b6d --- /dev/null +++ b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeCTAButton should render the component's initial state 1`] = ` +
+ +
+`; diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap new file mode 100644 index 000000000000..b406cafe0941 --- /dev/null +++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap @@ -0,0 +1,456 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PrepareBridgePage should render the component, with initial state 1`] = ` +
+
+
+
+
+ +
+
+ +
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+
+
+`; + +exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+
+
+`; diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx new file mode 100644 index 000000000000..5e42823c885b --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { BridgeCTAButton } from './bridge-cta-button'; + +describe('BridgeCTAButton', () => { + it("should render the component's initial state", () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], + }, + { fromTokenInputValue: 1 }, + ); + const { container, getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByText('Select token')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should render the component when tx is submittable', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Bridge')).toBeInTheDocument(); + expect(getByRole('button')).not.toBeDisabled(); + }); +}); diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx new file mode 100644 index 000000000000..fedcf4d4606a --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { Button } from '../../../components/component-library'; +import { + getFromAmount, + getFromChain, + getFromToken, + getToAmount, + getToChain, + getToToken, +} from '../../../ducks/bridge/selectors'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const BridgeCTAButton = () => { + const t = useI18nContext(); + const fromToken = useSelector(getFromToken); + const toToken = useSelector(getToToken); + + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + + const fromAmount = useSelector(getFromAmount); + const toAmount = useSelector(getToAmount); + + const isTxSubmittable = + fromToken && toToken && fromChain && toChain && fromAmount && toAmount; + + const label = useMemo(() => { + if (isTxSubmittable) { + return t('bridge'); + } + + return t('swapSelectToken'); + }, [isTxSubmittable]); + + return ( + + ); +}; diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx new file mode 100644 index 000000000000..811310590c71 --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { Hex } from '@metamask/utils'; +import { SwapsTokenObject } from '../../../../shared/constants/swaps'; +import { + Box, + Text, + TextField, + TextFieldType, +} from '../../../components/component-library'; +import { AssetPicker } from '../../../components/multichain/asset-picker-amount/asset-picker'; +import { TabName } from '../../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs'; +import CurrencyDisplay from '../../../components/ui/currency-display'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; +import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; +import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; +import Tooltip from '../../../components/ui/tooltip'; +import { SwapsEthToken } from '../../../selectors'; +import { + ERC20Asset, + NativeAsset, +} from '../../../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { zeroAddress } from '../../../__mocks__/ethereumjs-util'; +import { AssetType } from '../../../../shared/constants/transaction'; +import { + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, + CHAIN_ID_TOKEN_IMAGE_MAP, +} from '../../../../shared/constants/network'; +import useLatestBalance from '../../../hooks/bridge/useLatestBalance'; + +const generateAssetFromToken = ( + chainId: Hex, + tokenDetails: SwapsTokenObject | SwapsEthToken, +): ERC20Asset | NativeAsset => { + if ('iconUrl' in tokenDetails && tokenDetails.address !== zeroAddress()) { + return { + type: AssetType.token, + image: tokenDetails.iconUrl, + symbol: tokenDetails.symbol, + address: tokenDetails.address, + }; + } + + return { + type: AssetType.native, + image: + CHAIN_ID_TOKEN_IMAGE_MAP[ + chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP + ], + symbol: + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ + chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + ], + }; +}; + +export const BridgeInputGroup = ({ + className, + header, + token, + onAssetChange, + onAmountChange, + networkProps, + customTokenListGenerator, + amountFieldProps = {}, +}: { + className: string; + onAmountChange?: (value: string) => void; + token: SwapsTokenObject | SwapsEthToken | null; + amountFieldProps?: Pick< + React.ComponentProps, + 'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled' + >; +} & Pick< + React.ComponentProps, + 'networkProps' | 'header' | 'customTokenListGenerator' | 'onAssetChange' +>) => { + const t = useI18nContext(); + + const tokenFiatValue = useTokenFiatAmount( + token?.address || undefined, + amountFieldProps?.value?.toString() || '0x0', + token?.symbol, + { + showFiat: true, + }, + true, + ); + const ethFiatValue = useEthFiatAmount( + amountFieldProps?.value?.toString() || '0x0', + { showFiat: true }, + true, + ); + + const { formattedBalance } = useLatestBalance( + token, + networkProps?.network?.chainId, + ); + + return ( + + + + + { + onAmountChange?.(e.target.value); + }} + {...amountFieldProps} + /> + + + + + {formattedBalance ? `${t('balance')}: ${formattedBalance}` : ' '} + + + + + ); +}; diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss new file mode 100644 index 000000000000..1fa416df727f --- /dev/null +++ b/ui/pages/bridge/prepare/index.scss @@ -0,0 +1,170 @@ +@use "design-system"; + +.tokens-main-view-modal { + .multichain-asset-picker-list-item .mm-badge-wrapper__badge-container { + display: none; + } +} + +.prepare-bridge-page { + display: flex; + flex-flow: column; + flex: 1; + width: 100%; + + &__content { + display: flex; + flex-direction: column; + padding: 16px 0 16px 0; + border-radius: 8px; + border: 1px solid var(--color-border-muted); + } + + &__from, + &__to { + display: flex; + flex-direction: column; + gap: 4px; + justify-content: center; + padding: 8px 16px 8px 16px; + } + + &__input-row { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 298px; + gap: 16px; + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + .mm-text-field { + background-color: inherit; + + &--focused { + outline: none; + } + } + } + + &__amounts-row { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 298px; + height: 22px; + + p, + span { + color: var(--color-text-alternative); + font-size: 12px; + } + } + + .asset-picker { + border: 1px solid var(--color-border-muted); + height: 40px; + min-width: fit-content; + max-width: fit-content; + background-color: inherit; + + p { + font-size: 14px; + font-weight: 500; + } + + .mm-avatar-token { + height: 24px; + width: 24px; + border: 1px solid var(--color-border-muted); + } + + .mm-badge-wrapper__badge-container .mm-avatar-base { + height: 10px; + width: 10px; + border: none; + } + } + + .amount-input { + border: none; + + input { + text-align: right; + padding-right: 0; + font-size: 24px; + font-weight: 700; + + &:focus, + &:focus-visible { + outline: none; + } + } + + .mm-text-field--focused { + outline: none; + } + } + + &__switch-tokens { + display: flex; + justify-content: center; + align-items: center; + + &::before, + &::after { + content: ''; + border-top: 1px solid var(--color-border-muted); + flex-grow: 1; + } + + button { + border-radius: 50%; + padding: 10px; + border: 1px solid var(--color-border-muted); + transition: all 0.3s ease-in-out; + cursor: pointer; + width: 40px; + height: 40px; + + &:hover:enabled { + background: var(--color-background-default-hover); + + .mm-icon { + color: var(--color-icon-default); + } + } + + &:active { + background: var(--color-background-default-pressed); + + .mm-icon { + color: var(--color-icon-default); + } + } + + .rotate { + transform: rotate(360deg); + } + } + + .mm-icon { + color: var(--color-icon-alternative); + transition: all 0.3s ease-in-out; + } + + &:disabled { + cursor: not-allowed; + } + } +} diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx new file mode 100644 index 000000000000..82441aad218d --- /dev/null +++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { act } from '@testing-library/react'; +import { fireEvent, renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { createTestProviderTools } from '../../../../test/stub/provider'; +import PrepareBridgePage from './prepare-bridge-page'; + +describe('PrepareBridgePage', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'Ethereum', + chainId: CHAIN_IDS.MAINNET, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + it('should render the component, with initial state', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], + }, + {}, + ); + const { container, getByRole, getByTestId } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument(); + expect(getByRole('button', { name: /Select token/u })).toBeInTheDocument(); + + expect(getByTestId('from-amount')).toBeInTheDocument(); + expect(getByTestId('from-amount').closest('input')).not.toBeDisabled(); + await act(() => { + fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } }); + }); + expect(getByTestId('from-amount').closest('input')).toHaveValue(2); + + expect(getByTestId('to-amount')).toBeInTheDocument(); + expect(getByTestId('to-amount').closest('input')).toBeDisabled(); + + expect(getByTestId('switch-tokens').closest('button')).toBeDisabled(); + }); + + it('should render the component, with inputs set', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: { address: '0x3103910', decimals: 6 }, + toToken: { + iconUrl: 'http://url', + symbol: 'UNI', + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 6, + }, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { container, getByRole, getByTestId } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument(); + expect(getByRole('button', { name: /UNI/u })).toBeInTheDocument(); + + expect(getByTestId('from-amount')).toBeInTheDocument(); + expect(getByTestId('from-amount').closest('input')).not.toBeDisabled(); + expect(getByTestId('from-amount').closest('input')).toHaveValue(1); + await act(() => { + fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } }); + }); + expect(getByTestId('from-amount').closest('input')).toHaveValue(2); + + expect(getByTestId('to-amount')).toBeInTheDocument(); + expect(getByTestId('to-amount').closest('input')).toBeDisabled(); + + expect(getByTestId('switch-tokens').closest('button')).not.toBeDisabled(); + }); + + it('should throw an error if token decimals are not defined', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: { address: '0x3103910' }, + toToken: { + iconUrl: 'http://url', + symbol: 'UNI', + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + }, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + + expect(() => + renderWithProvider(, configureStore(mockStore)), + ).toThrow(); + }); +}); diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 6c66499ac9b9..2fdb11289c5b 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -1,24 +1,163 @@ -import React from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import { isEqual, shuffle } from 'lodash'; -import PrepareSwapPage from '../../swaps/prepare-swap-page/prepare-swap-page'; -import { getSelectedAccount, getTokenList } from '../../../selectors'; +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import classnames from 'classnames'; +import { + setFromChain, + setFromToken, + setFromTokenInputValue, + setToChain, + setToToken, + switchToAndFromTokens, +} from '../../../ducks/bridge/actions'; +import { + getFromAmount, + getFromChain, + getFromChains, + getFromToken, + getFromTokens, + getFromTopAssets, + getToAmount, + getToChain, + getToChains, + getToToken, + getToTokens, + getToTopAssets, +} from '../../../ducks/bridge/selectors'; +import { + Box, + ButtonIcon, + IconName, +} from '../../../components/component-library'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { TokenBucketPriority } from '../../../../shared/constants/swaps'; +import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering'; +import { setActiveNetwork } from '../../../store/actions'; +import { BlockSize } from '../../../helpers/constants/design-system'; +import { BridgeInputGroup } from './bridge-input-group'; -export const PrepareBridgePage = () => { - const selectedAccount = useSelector(getSelectedAccount, shallowEqual); - const { balance: ethBalance, address: selectedAccountAddress } = - selectedAccount; +const PrepareBridgePage = () => { + const dispatch = useDispatch(); - const tokenList = useSelector(getTokenList, isEqual); - const shuffledTokensList = shuffle(Object.values(tokenList)); + const t = useI18nContext(); + + const fromToken = useSelector(getFromToken); + const fromTokens = useSelector(getFromTokens); + const fromTopAssets = useSelector(getFromTopAssets); + + const toToken = useSelector(getToToken); + const toTokens = useSelector(getToTokens); + const toTopAssets = useSelector(getToTopAssets); + + const fromChains = useSelector(getFromChains); + const toChains = useSelector(getToChains); + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + + const fromAmount = useSelector(getFromAmount); + const toAmount = useSelector(getToAmount); + + const fromTokenListGenerator = useTokensWithFiltering( + fromTokens, + fromTopAssets, + TokenBucketPriority.owned, + fromChain?.chainId, + ); + const toTokenListGenerator = useTokensWithFiltering( + toTokens, + toTopAssets, + TokenBucketPriority.top, + toChain?.chainId, + ); + + const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); return ( -
- +
+ + { + dispatch(setFromTokenInputValue(e)); + }} + onAssetChange={(token) => dispatch(setFromToken(token))} + networkProps={{ + network: fromChain, + networks: fromChains, + onNetworkChange: (networkConfig) => { + dispatch( + setActiveNetwork( + networkConfig.rpcEndpoints[ + networkConfig.defaultRpcEndpointIndex + ].networkClientId, + ), + ); + dispatch(setFromChain(networkConfig.chainId)); + }, + }} + customTokenListGenerator={ + fromTokens && fromTopAssets ? fromTokenListGenerator : undefined + } + amountFieldProps={{ + testId: 'from-amount', + autoFocus: true, + value: fromAmount || undefined, + }} + /> + + + { + setRotateSwitchTokens(!rotateSwitchTokens); + const toChainClientId = + toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints + ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex] + .networkClientId + : undefined; + toChainClientId && dispatch(setActiveNetwork(toChainClientId)); + dispatch(switchToAndFromTokens({ fromChain })); + }} + /> + + + dispatch(setToToken(token))} + networkProps={{ + network: toChain, + networks: toChains, + onNetworkChange: (networkConfig) => { + dispatch(setToChain(networkConfig.chainId)); + }, + }} + customTokenListGenerator={ + toChain && toTokens && toTopAssets + ? toTokenListGenerator + : fromTokenListGenerator + } + amountFieldProps={{ + testId: 'to-amount', + readOnly: true, + disabled: true, + value: toAmount, + }} + /> +
); }; + +export default PrepareBridgePage; diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js index 822db143d29a..dda856d64abd 100644 --- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { getTokenTrackerLink } from '@metamask/etherscan-link'; import classnames from 'classnames'; import { PageContainerFooter } from '../../components/ui/page-container'; @@ -125,7 +125,7 @@ const ConfirmAddSuggestedNFT = () => { return dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); }), @@ -421,7 +421,7 @@ const ConfirmAddSuggestedNFT = () => { rejectPendingApproval( id, serializeError( - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ), ), ); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js index c3e1a3f73bf0..f099ea80bfd5 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { BannerAlert, Button, @@ -147,7 +147,7 @@ const ConfirmAddSuggestedToken = () => { dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ), ), 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/__snapshots__/confirm-legacy-gas-display.test.js.snap b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap index b586f2d5cd95..f6e40da8118c 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap @@ -115,5 +115,3 @@ exports[`ConfirmLegacyGasDisplay should match snapshot 1`] = `
`; - -exports[`ConfirmLegacyGasDisplay should match snapshot 2`] = `
`; 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..a37812899ec9 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -1,5 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ConfirmAlertModal } from '../../../../../components/app/alert-system/confirm-alert-modal'; @@ -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__/dapp-initiated-header.test.tsx.snap b/ui/pages/confirmations/components/confirm/header/__snapshots__/dapp-initiated-header.test.tsx.snap new file mode 100644 index 000000000000..26b0fed0b969 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/__snapshots__/dapp-initiated-header.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should match snapshot 1`] = ` +
+
+

+ Transfer request +

+
+
+ +
+
+
+
+`; 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..621318c6ebe2 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,79 @@ exports[`Header should match snapshot with signature confirmation 1`] = `
`; +exports[`Header should match snapshot with token transfer confirmation initiated in a dApp 1`] = ` +
+
+

+ Transfer request +

+
+
+ +
+
+
+
+`; + +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/dapp-initiated-header.test.tsx b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.test.tsx new file mode 100644 index 000000000000..4ae30d90936c --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { DefaultRootState } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; +import configureStore from '../../../../../store/store'; +import { DAppInitiatedHeader } from './dapp-initiated-header'; + +const render = ( + state: DefaultRootState = getMockTokenTransferConfirmState({}), +) => { + 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/dapp-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.tsx new file mode 100644 index 000000000000..3d4734659117 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/dapp-initiated-header.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { 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 { AdvancedDetailsButton } from './advanced-details-button'; + +export const DAppInitiatedHeader = () => { + const t = useI18nContext(); + + return ( + + + {t('transferRequest')} + + + + + + ); +}; 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..dacc432612b8 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,31 @@ 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 { DAppInitiatedHeader } from './dapp-initiated-header'; 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 ; + } + 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`] = ` 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..fed03a75e17e 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -1,14 +1,20 @@ -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 { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; 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 = () => { @@ -16,30 +22,49 @@ const ApproveInfo = () => { currentConfirmation: TransactionMeta; }; - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - const { isNFT } = useIsNFT(transactionMeta); 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 && ( )} - {showAdvancedDetails && } + 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..f88485e985b3 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,759 +294,12 @@ exports[` renders component for contract interaction requ
-
-
-

- Speed -

-
-
-
-
-

- 🦊 Market -

-

- - ~ - 0 sec - -

-
-
-
-
-
-`; - -exports[` renders component for contract interaction request 2`] = ` -
-
-
-
-
-

- Estimated changes -

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

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- ETH logo -
-

- ETH -

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

- Request from -

-
-
- -
-
-
-
-
-

- metamask.github.io -

-
-
-
-
-
-

- Interacting with -

-
-
- -
-
-
-
-
-
- -

- 0x88AA6...A5125 -

-
-
-
-
-
-
-
-
-

- Network fee -

-
-
- -
-
-
-
-
-

- 0.0001 ETH -

-

- $0.04 -

- -
-
-
-
-
-

- Speed -

-
-
-
-
-

- 🦊 Market -

-

- - ~ - 0 sec - -

-
-
-
-
-
-`; - -exports[` renders component for contract interaction request 3`] = ` -
-
-
-
-
-

- Estimated changes -

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

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

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

- Request from -

-
-
- -
-
-
-
-
-

- metamask.github.io -

-
-
-
-
-
-

- Interacting with -

-
-
- -
-
-
-
-
-
- -

- 0x88AA6...A5125 -

-
-
-
-
-
-
-
-

- Network fee -

-
-
- -
-
-
-
-
-

- 0.0001 ETH -

-

- $0.04 -

- -
-
-
-
{ const { currentConfirmation: transactionMeta } = useConfirmContext(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - if (!transactionMeta?.txParams) { return null; } @@ -33,7 +27,7 @@ const BaseTransactionInfo = () => { - {showAdvancedDetails && } + ); }; 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..1ed5e9c249ff --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts @@ -0,0 +1,126 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; +import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderHookWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; +import { useTokenValues } from './use-token-values'; +import { useDecodedTransactionData } from './useDecodedTransactionData'; + +jest.mock('../../../../hooks/useAssetDetails', () => ({ + ...jest.requireActual('../../../../hooks/useAssetDetails'), + useAssetDetails: jest.fn(), +})); + +jest.mock('./useDecodedTransactionData', () => ({ + ...jest.requireActual('./useDecodedTransactionData'), + useDecodedTransactionData: jest.fn(), +})); + +jest.mock( + '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate', + () => jest.fn(), +); + +describe('useTokenValues', () => { + const useAssetDetailsMock = jest.mocked(useAssetDetails); + const useDecodedTransactionDataMock = jest.mocked(useDecodedTransactionData); + const useTokenExchangeRateMock = jest.mocked(useTokenExchangeRate); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns native and fiat balances', async () => { + (useAssetDetailsMock as jest.Mock).mockImplementation(() => ({ + decimals: '10', + })); + (useDecodedTransactionDataMock as jest.Mock).mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: 'transfer', + params: [ + { + type: 'address', + value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', + }, + { + type: 'uint256', + value: 70000000000, + }, + ], + }, + ], + source: 'FourByte', + }, + })); + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( + new Numeric(0.91, 10), + ); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithConfirmContextProvider( + () => useTokenValues(transactionMeta), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + decodedTransferValue: 7, + fiatDisplayValue: '$6.37', + pending: false, + }); + }); + + it('returns undefined fiat balance if no token rate is returned', async () => { + (useAssetDetailsMock as jest.Mock).mockImplementation(() => ({ + decimals: '10', + })); + (useDecodedTransactionDataMock as jest.Mock).mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: 'transfer', + params: [ + { + type: 'address', + value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', + }, + { + type: 'uint256', + value: 70000000000, + }, + ], + }, + ], + source: 'FourByte', + }, + })); + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue(null); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithConfirmContextProvider( + () => useTokenValues(transactionMeta), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + decodedTransferValue: 7, + fiatDisplayValue: null, + pending: false, + }); + }); +}); 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..139a1e8116b9 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -0,0 +1,64 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { isHexString } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; +import { isBoolean } from 'lodash'; +import { useMemo, useState } from 'react'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; +import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; +import { useDecodedTransactionData } from './useDecodedTransactionData'; + +export const useTokenValues = (transactionMeta: TransactionMeta) => { + const { decimals } = useAssetDetails( + transactionMeta.txParams.to, + transactionMeta.txParams.from, + transactionMeta.txParams.data, + ); + + const decodedResponse = useDecodedTransactionData(); + const { value, pending } = decodedResponse; + + const decodedTransferValue = useMemo(() => { + if (!value || !decimals) { + 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 [exchangeRate, setExchangeRate] = useState(); + const fetchExchangeRate = async () => { + const result = await useTokenExchangeRate(transactionMeta?.txParams?.to); + + setExchangeRate(result); + }; + fetchExchangeRate(); + + const fiatValue = + exchangeRate && + decodedTransferValue && + exchangeRate.times(decodedTransferValue, 10).toNumber(); + const fiatFormatter = useFiatFormatter(); + const fiatDisplayValue = + fiatValue && fiatFormatter(fiatValue, { shorten: true }); + + return { + decodedTransferValue, + fiatDisplayValue, + pending, + }; +}; 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/hooks/useDecodedTransactionData.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts index 6934f893378d..5276e02eaad1 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.ts @@ -10,18 +10,24 @@ import { DecodedTransactionDataResponse } from '../../../../../../../shared/type import { useConfirmContext } from '../../../../context/confirm'; import { hasTransactionData } from '../../../../../../../shared/modules/transaction.utils'; -export function useDecodedTransactionData(): AsyncResult< - DecodedTransactionDataResponse | undefined -> { +export function useDecodedTransactionData( + transactionTypeFilter?: string, +): AsyncResult { const { currentConfirmation } = useConfirmContext(); + const currentTransactionType = currentConfirmation?.type; const chainId = currentConfirmation?.chainId as Hex; const contractAddress = currentConfirmation?.txParams?.to as Hex; const transactionData = currentConfirmation?.txParams?.data as Hex; const transactionTo = currentConfirmation?.txParams?.to as Hex; return useAsyncResult(async () => { - if (!hasTransactionData(transactionData) || !transactionTo) { + if ( + !hasTransactionData(transactionData) || + !transactionTo || + (transactionTypeFilter && + currentTransactionType !== transactionTypeFilter) + ) { return undefined; } diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts b/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts index 40aca7cf2d31..e4bfaad8d779 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useEIP1559TxFees.ts @@ -8,8 +8,11 @@ export const useEIP1559TxFees = ( maxFeePerGas: string; maxPriorityFeePerGas: string; } => { - const hexMaxFeePerGas = transactionMeta?.txParams?.maxFeePerGas; + const hexMaxFeePerGas = + transactionMeta.dappSuggestedGasFees?.maxFeePerGas || + transactionMeta?.txParams?.maxFeePerGas; const hexMaxPriorityFeePerGas = + transactionMeta.dappSuggestedGasFees?.maxPriorityFeePerGas || transactionMeta?.txParams?.maxPriorityFeePerGas; return useMemo(() => { diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts index 911cdb20118c..17c8ab8dd8f6 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.test.ts @@ -22,13 +22,13 @@ describe('useFeeCalculations', () => { expect(result.current).toMatchInlineSnapshot(` { "estimatedFeeFiat": "$0.00", - "estimatedFeeNative": "0 WEI", + "estimatedFeeNative": "0 ETH", "l1FeeFiat": "", "l1FeeNative": "", "l2FeeFiat": "", "l2FeeNative": "", "maxFeeFiat": "$0.00", - "maxFeeNative": "0 WEI", + "maxFeeNative": "0 ETH", } `); }); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts index ceb8a4b2d248..70bd2c0e3af2 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts @@ -8,14 +8,15 @@ import { addHexes, decGWEIToHexWEI, decimalToHex, - getEthConversionFromWeiHex, getValueFromWeiHex, multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; import { getConversionRate } from '../../../../../../ducks/metamask/metamask'; import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; import { getCurrentCurrency } from '../../../../../../selectors'; +import { getMultichainNetwork } from '../../../../../../selectors/multichain'; import { HEX_ZERO } from '../shared/constants'; import { useEIP1559TxFees } from './useEIP1559TxFees'; import { useSupportsEIP1559 } from './useSupportsEIP1559'; @@ -32,14 +33,18 @@ export function useFeeCalculations(transactionMeta: TransactionMeta) { const conversionRate = useSelector(getConversionRate); const fiatFormatter = useFiatFormatter(); + const multichainNetwork = useSelector(getMultichainNetwork); + const ticker = multichainNetwork?.network?.ticker; + const getFeesFromHex = useCallback( (hexFee: string) => { - const nativeCurrencyFee = - getEthConversionFromWeiHex({ + const nativeCurrencyFee = `${ + getValueFromWeiHex({ value: hexFee, fromCurrency: EtherDenomination.GWEI, numberOfDecimals: 4, - }) || `0 ${EtherDenomination.ETH}`; + }) || 0 + } ${ticker}`; const currentCurrencyFee = fiatFormatter( Number( @@ -114,11 +119,21 @@ export function useFeeCalculations(transactionMeta: TransactionMeta) { } // Logic for any network without L1 and L2 fee components - const minimumFeePerGas = addHexes( + let minimumFeePerGas = addHexes( decGWEIToHexWEI(estimatedBaseFee) || HEX_ZERO, decimalToHex(maxPriorityFeePerGas), ); + // `minimumFeePerGas` should never be higher than the `maxFeePerGas` + if ( + new Numeric(minimumFeePerGas, 16).greaterThan( + decimalToHex(maxFeePerGas), + 16, + ) + ) { + minimumFeePerGas = decimalToHex(maxFeePerGas); + } + const estimatedFee = multiplyHexes( supportsEIP1559 ? (minimumFeePerGas as Hex) : (gasPrice as Hex), gasLimit as Hex, diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts new file mode 100644 index 000000000000..efdf2b66ac56 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts @@ -0,0 +1,108 @@ +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 { useTokenDetails } from './useTokenDetails'; + +describe('useTokenDetails', () => { + 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( + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ + tokenImage: 'iconUrl', + tokenSymbol: 'symbol', + }); + }); + + 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( + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ + tokenImage: 'image', + tokenSymbol: 'symbol', + }); + }); + + 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( + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + { + ...mockState, + metamask: { + ...mockState.metamask, + tokenList: { + '0x076146c765189d51be3160a2140cf80bfc73ad68': { + iconUrl: 'tokenListIconUrl', + }, + }, + }, + }, + ); + + expect(result.current).toEqual({ + tokenImage: 'tokenListIconUrl', + tokenSymbol: 'symbol', + }); + }); + + 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( + () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ + tokenImage: undefined, + tokenSymbol: 'symbol', + }); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts new file mode 100644 index 000000000000..be9578496205 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts @@ -0,0 +1,27 @@ +import { TokenListMap } from '@metamask/assets-controllers'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { useSelector } from 'react-redux'; +import { useI18nContext } from '../../../../../../hooks/useI18nContext'; +import { getTokenList } from '../../../../../../selectors'; +import { SelectedToken } from '../shared/selected-token'; + +export const useTokenDetails = ( + transactionMeta: TransactionMeta, + selectedToken: SelectedToken, +) => { + const t = useI18nContext(); + + const tokenList = useSelector(getTokenList) as TokenListMap; + + const tokenImage = + selectedToken?.iconUrl || + selectedToken?.image || + tokenList[transactionMeta?.txParams?.to as string]?.iconUrl; + + const tokenSymbol = + selectedToken?.symbol || + tokenList[transactionMeta?.txParams?.to as string]?.symbol || + t('unknown'); + + return { tokenImage, tokenSymbol }; +}; diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts index 31802eb22feb..f5866a283935 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTransactionGasFeeEstimate.ts @@ -5,6 +5,7 @@ import { addHexes, multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; import { HEX_ZERO } from '../shared/constants'; @@ -28,15 +29,24 @@ export function useTransactionGasFeeEstimate( transactionMeta.dappSuggestedGasFees?.maxPriorityFeePerGas || transactionMeta.txParams?.maxPriorityFeePerGas || HEX_ZERO; + const maxFeePerGas = + transactionMeta.dappSuggestedGasFees?.maxFeePerGas || + transactionMeta.txParams?.maxFeePerGas || + HEX_ZERO; let gasEstimate: Hex; if (supportsEIP1559) { // Minimum Total Fee = (estimatedBaseFee + maxPriorityFeePerGas) * gasLimit - const minimumFeePerGas = addHexes( + let minimumFeePerGas = addHexes( estimatedBaseFee || HEX_ZERO, maxPriorityFeePerGas, ); + // `minimumFeePerGas` should never be higher than the `maxFeePerGas` + if (new Numeric(minimumFeePerGas, 16).greaterThan(maxFeePerGas, 16)) { + minimumFeePerGas = maxFeePerGas; + } + gasEstimate = multiplyHexes(minimumFeePerGas as Hex, gasLimit as Hex); } else { gasEstimate = multiplyHexes(gasPrice as Hex, gasLimit as Hex); 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..6902a6da9b1f 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 @@ -1,11 +1,10 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import React from 'react'; -import { useSelector } from 'react-redux'; import { useConfirmContext } from '../../../../context/confirm'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; 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'; @@ -15,13 +14,9 @@ const SetApprovalForAllInfo = () => { const { currentConfirmation: transactionMeta } = useConfirmContext(); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - const decodedResponse = useDecodedTransactionData(); - const { value } = decodedResponse; + const { value, pending } = decodedResponse; const isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll(value); @@ -31,6 +26,10 @@ const SetApprovalForAllInfo = () => { return null; } + if (pending) { + return ; + } + return ( <> {isRevokeSetApprovalForAll ? ( @@ -40,7 +39,7 @@ const SetApprovalForAllInfo = () => { )} - {showAdvancedDetails && } + ); }; 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..3a93bae1e26d 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 @@ -1,13 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` does not render component for advanced transaction details 1`] = ` +exports[` does not render component when the state property is false 1`] = `
`; + +exports[` renders component when the prop override is passed 1`] = `
does not render component for advanced transaction

does not render component for advanced transaction data-testid="advanced-details-data-section" >
does not render component for advanced transaction
`; -exports[` renders component for advanced transaction details 1`] = ` +exports[` renders component when the state property is true 1`] = `
renders component for advanced transaction details

renders component for advanced transaction details class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - 12 + undefined

-
@@ -187,7 +178,7 @@ exports[` renders component for advanced transaction details data-testid="advanced-details-data-section" >
', () => { const middleware = [thunk]; - it('does not render component for advanced transaction details', () => { - const state = mockState; + it('does not render component when the state property is false', () => { + const state = { + ...mockState, + metamask: { + ...mockState.metamask, + preferences: { + ...mockState.metamask.preferences, + showConfirmationAdvancedDetails: false, + }, + }, + }; + const mockStore = configureMockStore(middleware)(state); const { container } = renderWithConfirmContextProvider( , @@ -20,16 +30,18 @@ describe('', () => { expect(container).toMatchSnapshot(); }); - it('renders component for advanced transaction details', () => { + it('renders component when the state property is true', () => { const state = { ...mockState, metamask: { ...mockState.metamask, - useNonceField: true, - nextNonce: 1, - customNonceValue: '12', + preferences: { + ...mockState.metamask.preferences, + showConfirmationAdvancedDetails: true, + }, }, }; + const mockStore = configureMockStore(middleware)(state); const { container } = renderWithConfirmContextProvider( , @@ -38,4 +50,25 @@ describe('', () => { expect(container).toMatchSnapshot(); }); + + it('renders component when the prop override is passed', () => { + const state = { + ...mockState, + metamask: { + ...mockState.metamask, + preferences: { + ...mockState.metamask.preferences, + showConfirmationAdvancedDetails: false, + }, + }, + }; + + const mockStore = configureMockStore(middleware)(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx index 7e0cee721bb8..ebb0f69d75c1 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx @@ -16,6 +16,7 @@ import { showModal, updateCustomNonce, } from '../../../../../../../store/actions'; +import { selectConfirmationAdvancedDetailsOpen } from '../../../../../selectors/preferences'; import { TransactionData } from '../transaction-data/transaction-data'; const NonceDetails = () => { @@ -65,7 +66,19 @@ const NonceDetails = () => { ); }; -export const AdvancedDetails: React.FC = () => { +export const AdvancedDetails = ({ + overrideVisibility = false, +}: { + overrideVisibility?: boolean; +}) => { + const showAdvancedDetails = useSelector( + selectConfirmationAdvancedDetailsOpen, + ); + + if (!overrideVisibility && !showAdvancedDetails) { + return null; + } + return ( <> diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/__snapshots__/confirm-loader.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/__snapshots__/confirm-loader.test.tsx.snap new file mode 100644 index 000000000000..9ca7ce75defa --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/__snapshots__/confirm-loader.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 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`] = ` +
+
+ + + + + + + + + +
+
+`; 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..8944a84b770e --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx @@ -0,0 +1,31 @@ +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..40c571d4bc75 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -0,0 +1,88 @@ +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 { getWatchedToken } from '../../../../../../../selectors'; +import { MultichainState } from '../../../../../../../selectors/multichain'; +import { useConfirmContext } from '../../../../../context/confirm'; +import { useTokenDetails } from '../../hooks/useTokenDetails'; +import { useTokenValues } from '../../hooks/use-token-values'; +import { ConfirmLoader } from '../confirm-loader/confirm-loader'; + +const SendHeading = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + const selectedToken = useSelector((state: MultichainState) => + getWatchedToken(transactionMeta)(state), + ); + const { tokenImage, tokenSymbol } = useTokenDetails( + transactionMeta, + selectedToken, + ); + const { decodedTransferValue, fiatDisplayValue, pending } = + useTokenValues(transactionMeta); + + const TokenImage = ( + + ); + + const TokenValue = ( + <> + {`${decodedTransferValue || ''} ${tokenSymbol}`} + {fiatDisplayValue && ( + + {fiatDisplayValue} + + )} + + ); + + if (pending) { + return ; + } + + 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-details-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap new file mode 100644 index 000000000000..c545cea1f66d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-details-section.test.tsx.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenDetailsSection renders correctly 1`] = ` +
+
+
+
+
+

+ Network +

+
+
+
+
+ G +
+

+ Goerli +

+
+
+
+
+
+

+ Interacting with +

+
+
+
+
+ +

+ 0x07614...3ad68 +

+
+
+
+
+
+`; 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..e1d19241252b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap @@ -0,0 +1,388 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenTransferInfo renders correctly 1`] = ` +
+
+
+ ? +
+

+ Unknown +

+
+
+
+
+
+ +

+ 0x2e0D7...5d09B +

+
+
+ +
+
+
+
+
+
+

+ Estimated changes +

+
+
+ +
+
+
+
+
+
+
+
+

+ You send +

+
+
+
+
+
+

+ - 4 +

+
+
+
+
+
+
+ E +
+

+ ETH +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network +

+
+
+
+
+ G +
+

+ Goerli +

+
+
+
+
+
+

+ Interacting with +

+
+
+
+
+ +

+ 0x07614...3ad68 +

+
+
+
+
+
+
+
+
+

+ Network fee +

+
+
+ +
+
+
+
+
+

+ 0.0001 ETH +

+

+ $0.04 +

+ +
+
+
+
+
+

+ Speed +

+
+
+
+
+

+ 🦊 Market +

+

+ + ~ + 0 sec + +

+
+
+
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap new file mode 100644 index 000000000000..23cddb2b59b2 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +
+
+
+
+
+ +

+ 0x2e0D7...5d09B +

+
+
+ +
+
+ +

+ 0x6B175...71d0F +

+
+
+
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.test.tsx new file mode 100644 index 000000000000..4188ea62bc84 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.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 { TokenDetailsSection } from './token-details-section'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +describe('TokenDetailsSection', () => { + 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-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx new file mode 100644 index 000000000000..48a5f2dad74c --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx @@ -0,0 +1,76 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../shared/constants/network'; +import { + ConfirmInfoRow, + ConfirmInfoRowAddress, +} from '../../../../../../components/app/confirm/info/row'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { + AvatarNetwork, + AvatarNetworkSize, + Box, + Text, +} from '../../../../../../components/component-library'; +import { + AlignItems, + BlockSize, + BorderColor, + Display, + FlexWrap, + TextColor, + TextVariant, +} from '../../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../../hooks/useI18nContext'; +import { getNetworkConfigurationsByChainId } from '../../../../../../selectors'; +import { useConfirmContext } from '../../../../context/confirm'; + +export const TokenDetailsSection = () => { + const t = useI18nContext(); + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const { chainId } = transactionMeta; + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const networkName = networkConfigurations[chainId].name; + + const networkRow = ( + + + + + {networkName} + + + + ); + + const tokenRow = ( + + + + ); + + return ( + + {networkRow} + {tokenRow} + + ); +}; 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..1cb5f3b40ab2 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { Box } from '../../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, +} from '../../../../../../helpers/constants/design-system'; +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..76c419cbb0be --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx @@ -0,0 +1,41 @@ +import { screen, waitFor } from '@testing-library/react'; +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 { tEn } from '../../../../../../../test/lib/i18n-helpers'; +import TokenTransferInfo from './token-transfer'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +jest.mock('../../../../../../store/actions', () => ({ + ...jest.requireActual('../../../../../../store/actions'), + getGasFeeTimeEstimate: jest.fn().mockResolvedValue({ + lowerTimeBound: 0, + upperTimeBound: 60000, + }), +})); + +describe('TokenTransferInfo', () => { + it('renders correctly', async () => { + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore([])(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + await waitFor(() => { + expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); + }); + + 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..b89e87350a36 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx @@ -0,0 +1,38 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { useConfirmContext } from '../../../../context/confirm'; +import { SimulationDetails } from '../../../simulation-details'; +import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; +import SendHeading from '../shared/send-heading/send-heading'; +import { TokenDetailsSection } from './token-details-section'; +import { TransactionFlowSection } from './transaction-flow-section'; + +const TokenTransferInfo = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const isWalletInitiated = transactionMeta.origin === 'metamask'; + + return ( + <> + + + {!isWalletInitiated && ( + + + + )} + + + + + ); +}; + +export default TokenTransferInfo; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx new file mode 100644 index 000000000000..c23d3645abd3 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx @@ -0,0 +1,48 @@ +import { TransactionType } from '@metamask/transaction-controller'; +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 { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; +import { TransactionFlowSection } from './transaction-flow-section'; + +jest.mock('../hooks/useDecodedTransactionData', () => ({ + ...jest.requireActual('../hooks/useDecodedTransactionData'), + useDecodedTransactionData: jest.fn(), +})); + +describe('', () => { + const useDecodedTransactionDataMock = jest.fn().mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: TransactionType.tokenMethodTransfer, + params: [ + { + name: 'dst', + type: 'address', + value: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + }, + { name: 'wad', type: 'uint256', value: 0 }, + ], + }, + ], + source: 'Sourcify', + }, + })); + + (useDecodedTransactionData as jest.Mock).mockImplementation( + useDecodedTransactionDataMock, + ); + + 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/transaction-flow-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx new file mode 100644 index 000000000000..de0e928c10f8 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx @@ -0,0 +1,61 @@ +import { NameType } from '@metamask/name-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import Name from '../../../../../../components/app/name'; +import { + Box, + Icon, + IconName, + IconSize, +} from '../../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + IconColor, + JustifyContent, +} from '../../../../../../helpers/constants/design-system'; +import { useConfirmContext } from '../../../../context/confirm'; +import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; + +export const TransactionFlowSection = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const { value, pending } = useDecodedTransactionData(); + + const recipientAddress = value?.data[0].params.find( + (param) => param.type === 'address', + )?.value; + + if (pending) { + return ; + } + + return ( + + + + + {recipientAddress && ( + + )} + + + ); +}; 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" >
-
+
@@ -125,3 +127,120 @@ exports[`PermitSimulation renders component correctly 1`] = `
`; + +exports[`PermitSimulation renders correctly for NFT permit 1`] = ` +
+
+
+
+
+

+ Estimated changes +

+
+
+ +
+
+
+
+
+

+ You're giving someone else permission to withdraw NFTs from your account. +

+
+
+
+
+
+

+ Withdraw +

+
+
+
+
+
+
+
+

+ #3606393 +

+
+
+
+
+ +

+ 0xC3644...1FE88 +

+
+
+
+
+
+
+
+
+
+`; 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..0d67715867d9 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 @@ -4,17 +4,29 @@ 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 { + permitNFTSignatureMsg, + permitSignatureMsg, +} from '../../../../../../../../test/data/confirmations/typed_sign'; +import { memoizedGetTokenStandardAndDetails } from '../../../../../utils/token'; import PermitSimulation from './permit-simulation'; jest.mock('../../../../../../../store/actions', () => { return { - getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 2 }), + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 2, standard: 'ERC20' }), }; }); describe('PermitSimulation', () => { + afterEach(() => { + jest.clearAllMocks(); + + /** Reset memoized function using getTokenStandardAndDetails for each test */ + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); + }); + it('renders component correctly', async () => { const state = getMockTypedSignConfirmStateForRequest(permitSignatureMsg); const mockStore = configureMockStore([])(state); @@ -29,4 +41,20 @@ describe('PermitSimulation', () => { expect(container).toMatchSnapshot(); }); }); + + it('renders correctly for NFT permit', async () => { + const state = getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg); + const mockStore = configureMockStore([])(state); + + await act(async () => { + const { container, findByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(await findByText('Withdraw')).toBeInTheDocument(); + expect(await findByText('#3606393')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx index 231997d18547..44131ec18fbf 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx @@ -45,8 +45,10 @@ const PermitSimulation: React.FC = () => { const { domain: { verifyingContract }, message, + message: { tokenId }, primaryType, } = parseTypedDataMessage(msgData as string); + const isNFT = tokenId !== undefined; const tokenDetails = extractTokenDetailsByPrimaryType(message, primaryType); @@ -68,7 +70,9 @@ const PermitSimulation: React.FC = () => { ); const SpendingCapRow = ( - + {Array.isArray(tokenDetails) ? ( = () => { )} @@ -99,7 +104,9 @@ const PermitSimulation: React.FC = () => { ); 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.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx index f6af7357502d..e8e48c1ca6f9 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.test.tsx @@ -4,14 +4,24 @@ import configureMockStore from 'redux-mock-store'; import mockState from '../../../../../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../../../../../test/lib/render-helpers'; +import useTrackERC20WithoutDecimalInformation from '../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; import PermitSimulationValueDisplay from './value-display'; jest.mock('../../../../../../../../store/actions', () => { return { - getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 4 }), + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 4, standard: 'ERC20' }), }; }); +jest.mock( + '../../../../../../hooks/useTrackERC20WithoutDecimalInformation', + () => { + return jest.fn(); + }, +); + describe('PermitSimulationValueDisplay', () => { it('renders component correctly', async () => { const mockStore = configureMockStore([])(mockState); @@ -29,4 +39,20 @@ describe('PermitSimulationValueDisplay', () => { expect(container).toMatchSnapshot(); }); }); + + it('should invoke method to track missing decimal information for ERC20 tokens', async () => { + const mockStore = configureMockStore([])(mockState); + + await act(async () => { + renderWithProvider( + , + mockStore, + ); + + expect(useTrackERC20WithoutDecimalInformation).toHaveBeenCalled(); + }); + }); }); 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..e95edc03087b 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 @@ -2,8 +2,9 @@ import React, { useMemo } from 'react'; import { NameType } from '@metamask/name-controller'; import { Hex } from '@metamask/utils'; import { captureException } from '@sentry/browser'; -import { shortenString } from '../../../../../../../../helpers/utils/util'; +import { MetaMetricsEventLocation } from '../../../../../../../../../shared/constants/metametrics'; +import { shortenString } from '../../../../../../../../helpers/utils/util'; import { calcTokenAmount } from '../../../../../../../../../shared/lib/transactions-controller-utils'; import useTokenExchangeRate from '../../../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; import { IndividualFiatDisplay } from '../../../../../simulation-details/fiat-display'; @@ -11,7 +12,8 @@ import { formatAmount, formatAmountMaxPrecision, } from '../../../../../simulation-details/formatAmount'; -import { useAsyncResult } from '../../../../../../../../hooks/useAsyncResult'; +import { useGetTokenStandardAndDetails } from '../../../../../../hooks/useGetTokenStandardAndDetails'; +import useTrackERC20WithoutDecimalInformation from '../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; import { Box, @@ -27,7 +29,7 @@ import { TextAlign, } from '../../../../../../../../helpers/constants/design-system'; import Name from '../../../../../../../../components/app/name/name'; -import { fetchErc20Decimals } from '../../../../../../utils/token'; +import { TokenDetailsERC20 } from '../../../../../../utils/token'; type PermitSimulationValueDisplayParams = { /** The primaryType of the typed sign message */ @@ -41,21 +43,27 @@ type PermitSimulationValueDisplayParams = { tokenContract: Hex | string; /** The token amount */ - value: number | string; + value?: number | string; + + /** The tokenId for NFT */ + tokenId?: string; }; const PermitSimulationValueDisplay: React.FC< PermitSimulationValueDisplayParams -> = ({ primaryType, tokenContract, value }) => { +> = ({ primaryType, tokenContract, value, tokenId }) => { const exchangeRate = useTokenExchangeRate(tokenContract); - const { value: tokenDecimals } = useAsyncResult( - async () => await fetchErc20Decimals(tokenContract), - [tokenContract], + const tokenDetails = useGetTokenStandardAndDetails(tokenContract); + useTrackERC20WithoutDecimalInformation( + tokenContract, + tokenDetails as TokenDetailsERC20, + MetaMetricsEventLocation.SignatureConfirmation, ); + const { decimalsNumber: tokenDecimals } = tokenDetails; const fiatValue = useMemo(() => { - if (exchangeRate && value) { + if (exchangeRate && value && !tokenId) { const tokenAmount = calcTokenAmount(value, tokenDecimals); return exchangeRate.times(tokenAmount).toNumber(); } @@ -63,7 +71,7 @@ const PermitSimulationValueDisplay: React.FC< }, [exchangeRate, tokenDecimals, value]); const { tokenValue, tokenValueMaxPrecision } = useMemo(() => { - if (!value) { + if (!value || tokenId) { return { tokenValue: null, tokenValueMaxPrecision: null }; } @@ -107,16 +115,22 @@ const PermitSimulationValueDisplay: React.FC< style={{ paddingTop: '1px', paddingBottom: '1px' }} textAlign={TextAlign.Center} > - {shortenString(tokenValue || '', { - truncatedCharLimit: 15, - truncatedStartChars: 15, - truncatedEndChars: 0, - skipCharacterInEnd: true, - })} + {tokenValue !== null && + shortenString(tokenValue || '', { + truncatedCharLimit: 15, + truncatedStartChars: 15, + truncatedEndChars: 0, + skipCharacterInEnd: true, + })} + {tokenId && `#${tokenId}`} - + {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..de0637a9f641 100644 --- a/ui/pages/confirmations/components/confirm/nav/nav.tsx +++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx @@ -1,4 +1,4 @@ -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -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(); @@ -78,7 +78,7 @@ const Nav = () => { dispatch( rejectPendingApproval( conf.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); }); 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" >
| TreeData[]; @@ -72,9 +71,15 @@ const FIELD_DATE_PRIMARY_TYPES: Record = { */ const NONE_DATE_VALUE = -1; -const getTokenDecimalsOfDataTree = async ( +/** + * 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 getTokenContractInDataTree = ( dataTreeData: Record | TreeData[], -): Promise => { +): Hex | undefined => { if (Array.isArray(dataTreeData)) { return undefined; } @@ -85,25 +90,22 @@ const getTokenDecimalsOfDataTree = async ( return undefined; } - return await fetchErc20Decimals(tokenContract); + return tokenContract; }; export const DataTree = ({ data, primaryType, - tokenDecimals = 0, + tokenDecimals: tokenDecimalsProp, }: { data: Record | TreeData[]; primaryType?: PrimaryType; tokenDecimals?: number; }) => { - const { value: decimalsResponse } = useAsyncResult( - async () => await getTokenDecimalsOfDataTree(data), - [data], - ); - - const tokenContractDecimals = - typeof decimalsResponse === 'number' ? decimalsResponse : undefined; + const tokenContract = getTokenContractInDataTree(data); + const { decimalsNumber } = useGetTokenStandardAndDetails(tokenContract); + const tokenDecimals = + typeof decimalsNumber === 'number' ? decimalsNumber : tokenDecimalsProp; return ( @@ -122,7 +124,7 @@ export const DataTree = ({ primaryType={primaryType} value={value} type={type} - tokenDecimals={tokenContractDecimals ?? tokenDecimals} + tokenDecimals={tokenDecimals} /> } @@ -153,7 +155,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('should match snapshot', () => { - const { container } = render( + const { container } = renderWithProvider( , + mockStore, ); expect(container).toMatchSnapshot(); }); it('should return null if data is not defined', () => { - const { container } = render( + const { container } = renderWithProvider( , + mockStore, ); expect(container).toBeEmptyDOMElement(); }); diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data/__snapshots__/typedSignData.test.tsx.snap b/ui/pages/confirmations/components/confirm/row/typed-sign-data/__snapshots__/typedSignData.test.tsx.snap index 5070f3c49733..2428c54d6bbd 100644 --- a/ui/pages/confirmations/components/confirm/row/typed-sign-data/__snapshots__/typedSignData.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data/__snapshots__/typedSignData.test.tsx.snap @@ -6,7 +6,7 @@ exports[`ConfirmInfoRowTypedSignData should match snapshot 1`] = ` class="mm-box mm-box--width-full" >
= ({ snapId, interfaceId, loading, + isExpanded, }) => { const t = useI18nContext(); const { name: snapName } = useSelector((state) => @@ -57,6 +59,7 @@ export const SnapInsight: React.FunctionComponent = ({ { mockStore, ); - fireEvent.click(getByText('Insights from')); - expect(container).toMatchSnapshot(); expect(getByText('Hello world!')).toBeDefined(); }); @@ -79,8 +76,6 @@ describe('SnapsSection', () => { mockStore, ); - fireEvent.click(getByText('Insights from')); - expect(container).toMatchSnapshot(); expect(getByText('Hello world again!')).toBeDefined(); }); diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx index 896411ad4dec..b838255237e6 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/snaps-section.tsx @@ -23,12 +23,13 @@ export const SnapsSection = () => { gap={4} marginBottom={4} > - {data.map(({ snapId, interfaceId, loading }) => ( + {data.map(({ snapId, interfaceId, loading }, index) => ( ))} diff --git a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts new file mode 100644 index 000000000000..9bea069d0935 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.test.ts @@ -0,0 +1,21 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { CONTRACT_INTERACTION_SENDER_ADDRESS } from '../../../../../../../test/data/confirmations/contract-interaction'; +import { genUnapprovedApproveConfirmation } from '../../../../../../../test/data/confirmations/token-approve'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderHookWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import { useCurrentSpendingCap } from './useCurrentSpendingCap'; + +describe('useCurrentSpendingCap', () => { + 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..3d4d6672940d 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, @@ -11,7 +11,10 @@ import { getMockTypedSignConfirmStateForRequest, } from '../../../../../../test/data/confirmations/helper'; import { unapprovedPersonalSignMsg } from '../../../../../../test/data/confirmations/personal_sign'; -import { permitSignatureMsg } from '../../../../../../test/data/confirmations/typed_sign'; +import { + permitNFTSignatureMsg, + permitSignatureMsg, +} from '../../../../../../test/data/confirmations/typed_sign'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { tEn } from '../../../../../../test/lib/i18n-helpers'; import { @@ -22,6 +25,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 })), })); @@ -55,6 +74,21 @@ describe('ConfirmTitle', () => { ).toBeInTheDocument(); }); + it('should render the title and description for a NFT permit signature', () => { + const mockStore = configureMockStore([])( + getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg), + ); + const { getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(getByText('Withdrawal request')).toBeInTheDocument(); + expect( + getByText('This site wants permission to withdraw your NFTs'), + ).toBeInTheDocument(); + }); + it('should render the title and description for typed signature', () => { const mockStore = configureMockStore([])(getMockTypedSignConfirmState()); const { getByText } = renderWithConfirmContextProvider( @@ -119,7 +153,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 +162,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 +180,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 +219,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 +227,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..a926c0f6b482 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -4,8 +4,8 @@ import { } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; +import { TokenStandard } from '../../../../../../shared/constants/transaction'; import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; -import { getHighestSeverity } from '../../../../../components/app/alert-system/utils'; import { Box, Text } from '../../../../../components/component-library'; import { TextAlign, @@ -14,60 +14,60 @@ import { } from '../../../../../helpers/constants/design-system'; import useAlerts from '../../../../../hooks/useAlerts'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { TypedSignSignaturePrimaryTypes } from '../../../constants'; import { useConfirmContext } from '../../../context/confirm'; import { Confirmation, SignatureRequestType } from '../../../types/confirm'; -import { - isPermitSignatureRequest, - isSIWESignatureRequest, -} from '../../../utils'; +import { isSIWESignatureRequest } from '../../../utils'; +import { useTypedSignSignatureInfo } from '../../../hooks/useTypedSignSignatureInfo'; import { useIsNFT } from '../info/approve/hooks/use-is-nft'; import { useDecodedTransactionData } from '../info/hooks/useDecodedTransactionData'; import { getIsRevokeSetApprovalForAll } from '../info/utils'; +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) => ( + + + + ))} ); } type IntlFunction = (str: string) => string; +// todo: getTitle and getDescription can be merged to remove code duplication. + const getTitle = ( t: IntlFunction, confirmation?: Confirmation, isNFT?: boolean, + customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return t('confirmTitleTransaction'); @@ -79,13 +79,20 @@ const getTitle = ( } return t('confirmTitleSignature'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitlePermitTokens') - : t('confirmTitleSignature'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('setApprovalForAllRedesignedTitle'); + } + return t('confirmTitlePermitTokens'); + } + return t('confirmTitleSignature'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleApproveTransaction'); } + if (customSpendingCap === '0') { + return t('confirmTitleRevokeApproveTransaction'); + } return t('confirmTitlePermitTokens'); case TransactionType.tokenMethodIncreaseAllowance: return t('confirmTitlePermitTokens'); @@ -103,8 +110,16 @@ const getDescription = ( t: IntlFunction, confirmation?: Confirmation, isNFT?: boolean, + customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return ''; @@ -116,13 +131,20 @@ const getDescription = ( } return t('confirmTitleDescSign'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitleDescPermitSignature') - : t('confirmTitleDescSign'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('confirmTitleDescApproveTransaction'); + } + return t('confirmTitleDescPermitSignature'); + } + return t('confirmTitleDescSign'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleDescApproveTransaction'); } + if (customSpendingCap === '0') { + return ''; + } return t('confirmTitleDescERC20ApproveTransaction'); case TransactionType.tokenMethodIncreaseAllowance: return t('confirmTitleDescPermitSignature'); @@ -143,15 +165,25 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); + const { primaryType, tokenStandard } = useTypedSignSignatureInfo( + currentConfirmation as SignatureRequestType, + ); + + const { customSpendingCap, pending: spendingCapPending } = + useCurrentSpendingCap(currentConfirmation); + let isRevokeSetApprovalForAll = false; + let revokePending = false; + const decodedResponse = useDecodedTransactionData( + TransactionType.tokenMethodSetApprovalForAll, + ); if ( currentConfirmation?.type === TransactionType.tokenMethodSetApprovalForAll ) { - const decodedResponse = useDecodedTransactionData(); - isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll( decodedResponse.value, ); + revokePending = decodedResponse.pending; } const title = useMemo( @@ -160,9 +192,22 @@ const ConfirmTitle: React.FC = memo(() => { t as IntlFunction, currentConfirmation, isNFT, + customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, + primaryType, + tokenStandard, ), - [currentConfirmation, isNFT, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + primaryType, + tokenStandard, + ], ); const description = useMemo( @@ -171,9 +216,22 @@ const ConfirmTitle: React.FC = memo(() => { t as IntlFunction, currentConfirmation, isNFT, + customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, + primaryType, + tokenStandard, ), - [currentConfirmation, isNFT, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + primaryType, + tokenStandard, + ], ); if (!currentConfirmation) { @@ -183,21 +241,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-original/signature-request-original.component.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js index f9c9dbe9c0a1..026135a52685 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ObjectInspector } from 'react-inspector'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; import LedgerInstructionField from '../ledger-instruction-field'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; @@ -275,7 +275,7 @@ export default class SignatureRequestOriginal extends Component { await rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); @@ -304,7 +304,7 @@ export default class SignatureRequestOriginal extends Component { onCancel={async () => { await rejectPendingApproval( txData.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js index 817f9f8699d4..0ac6b877fa72 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js @@ -11,6 +11,7 @@ import { } from '../../../../store/actions'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) // eslint-disable-next-line import/order +import { getErrorMessage } from '../../../../../shared/modules/error'; import { mmiActionsFactory, setPersonalMessageInProgress, @@ -173,7 +174,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { } catch (err) { await dispatchProps.setWaitForConfirmDeepLinkDialog(true); await dispatchProps.showTransactionsFailedModal({ - errorMessage: err.message, + errorMessage: getErrorMessage(err), closeNotification: true, operationFailed: true, }); diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js index 1ade6dd1a630..e1effe5c8ff3 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js @@ -4,7 +4,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import log from 'loglevel'; import { isValidSIWEOrigin } from '@metamask/controller-utils'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { BannerAlert, Text } from '../../../../components/component-library'; import Popover from '../../../../components/ui/popover'; import Checkbox from '../../../../components/ui/check-box'; @@ -102,7 +102,7 @@ export default function SignatureRequestSIWE({ txData, warnings }) { await dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); } catch (e) { diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js index f15c7045e2d7..ce6967b50f70 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.js @@ -8,7 +8,7 @@ import { } from 'react-redux'; import PropTypes from 'prop-types'; import { memoize } from 'lodash'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { resolvePendingApproval, completedTx, @@ -176,7 +176,7 @@ const SignatureRequest = ({ txData, warnings }) => { await dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); trackEvent({ 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/useBalanceChanges.test.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts index 10e4cca518b7..5dc0be870538 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts @@ -9,7 +9,7 @@ import { TokenStandard } from '../../../../../shared/constants/transaction'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { getTokenStandardAndDetails } from '../../../../store/actions'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; -import { fetchErc20Decimals } from '../../utils/token'; +import { memoizedGetTokenStandardAndDetails } from '../../utils/token'; import { useBalanceChanges } from './useBalanceChanges'; import { FIAT_UNAVAILABLE } from './types'; @@ -92,7 +92,7 @@ describe('useBalanceChanges', () => { afterEach(() => { /** Reset memoized function for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); describe('pending states', () => { 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 fc0b5933c2ff..1ae7eaaeb33d 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, @@ -179,7 +178,7 @@ export default class ConfirmTransactionBase extends Component { isUserOpContractDeployError: PropTypes.bool, useMaxValue: PropTypes.bool, maxValue: PropTypes.string, - smartTransactionsOptInStatus: PropTypes.bool, + smartTransactionsPreferenceEnabled: PropTypes.bool, currentChainSupportsSmartTransactions: PropTypes.bool, selectedNetworkClientId: PropTypes.string, isSmartTransactionsEnabled: PropTypes.bool, @@ -406,7 +405,6 @@ export default class ConfirmTransactionBase extends Component { nextNonce, getNextNonce, txData, - useNativeCurrencyAsPrimaryCurrency, primaryTotalTextOverrideMaxAmount, showLedgerSteps, nativeCurrency, @@ -466,7 +464,6 @@ export default class ConfirmTransactionBase extends Component { type={PRIMARY} key="total-max-amount" value={getTotalAmount(useMaxFee)} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); } @@ -475,9 +472,8 @@ export default class ConfirmTransactionBase extends Component { const primaryTotal = useMaxFee ? primaryTotalTextOverrideMaxAmount : primaryTotalTextOverride; - const totalMaxAmount = useNativeCurrencyAsPrimaryCurrency - ? primaryTotal - : secondaryTotalTextOverride; + + const totalMaxAmount = primaryTotal; return isBoldTextAndNotOverridden ? ( {totalMaxAmount} @@ -507,14 +503,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 = @@ -1025,7 +1019,7 @@ export default class ConfirmTransactionBase extends Component { txData: { origin, chainId: txChainId } = {}, getNextNonce, tryReverseResolveAddress, - smartTransactionsOptInStatus, + smartTransactionsPreferenceEnabled, currentChainSupportsSmartTransactions, setSwapsFeatureFlags, fetchSmartTransactionsLiveness, @@ -1077,7 +1071,10 @@ export default class ConfirmTransactionBase extends Component { window.addEventListener('beforeunload', this._beforeUnloadForGasPolling); - if (smartTransactionsOptInStatus && currentChainSupportsSmartTransactions) { + if ( + smartTransactionsPreferenceEnabled && + currentChainSupportsSmartTransactions + ) { // TODO: Fetching swaps feature flags, which include feature flags for smart transactions, is only a short-term solution. // Long-term, we want to have a new proxy service specifically for feature flags. Promise.all([ 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..e06090f48e75 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, @@ -60,10 +59,10 @@ import { } from '../../../selectors'; import { getCurrentChainSupportsSmartTransactions, - getSmartTransactionsOptInStatus, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) getSmartTransactionsEnabled, ///: END:ONLY_INCLUDE_IF + getSmartTransactionsPreferenceEnabled, } from '../../../../shared/modules/selectors'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { @@ -186,7 +185,8 @@ const mapStateToProps = (state, ownProps) => { data, } = (transaction && transaction.txParams) || txParams; const accounts = getMetaMaskAccounts(state); - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); + const smartTransactionsPreferenceEnabled = + getSmartTransactionsPreferenceEnabled(state); const currentChainSupportsSmartTransactions = getCurrentChainSupportsSmartTransactions(state); @@ -266,7 +266,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 +346,6 @@ const mapStateToProps = (state, ownProps) => { noGasPrice, supportsEIP1559, gasIsLoading: isGasEstimatesLoading || gasLoadingAnimationIsShowing, - useNativeCurrencyAsPrimaryCurrency, maxFeePerGas: gasEstimationObject.maxFeePerGas, maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas, baseFeePerGas: gasEstimationObject.baseFeePerGas, @@ -367,7 +365,7 @@ const mapStateToProps = (state, ownProps) => { isUserOpContractDeployError, useMaxValue, maxValue, - smartTransactionsOptInStatus, + smartTransactionsPreferenceEnabled, currentChainSupportsSmartTransactions, hasPriorityApprovalRequest, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 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" >
({ @@ -35,7 +34,7 @@ describe('Confirm', () => { jest.resetAllMocks(); /** Reset memoized function using getTokenStandardAndDetails for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); it('should render', () => { @@ -60,7 +59,7 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); @@ -81,13 +80,15 @@ describe('Confirm', () => { 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 () => { @@ -102,12 +103,12 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); 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 () => { @@ -143,16 +146,15 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); 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(); }); }); @@ -168,16 +170,15 @@ describe('Confirm', () => { jest.spyOn(actions, 'getTokenStandardAndDetails').mockResolvedValue({ decimals: '2', - standard: 'erc20', + standard: 'ERC20', }); 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/confirmation.js b/ui/pages/confirmations/confirmation/confirmation.js index b37743c256bc..4bb1f4f7d203 100644 --- a/ui/pages/confirmations/confirmation/confirmation.js +++ b/ui/pages/confirmations/confirmation/confirmation.js @@ -14,6 +14,7 @@ import { produce } from 'immer'; import log from 'loglevel'; import { ApprovalType } from '@metamask/controller-utils'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; +import { CHAIN_SPEC_URL } from '../../../../shared/constants/network'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { MetaMetricsEventCategory, @@ -217,16 +218,19 @@ export default function ConfirmationPage({ ); const [approvalFlowLoadingText, setApprovalFlowLoadingText] = useState(null); - const [currentPendingConfirmation, setCurrentPendingConfirmation] = - useState(0); const { id } = useParams(); - const pendingRoutedConfirmation = pendingConfirmations.find( + const pendingRoutedConfirmation = pendingConfirmations.findIndex( (confirmation) => confirmation.id === id, ); - // Confirmations that are directly routed to get priority and will be shown above the current queue. - const pendingConfirmation = - pendingRoutedConfirmation ?? - pendingConfirmations[currentPendingConfirmation]; + + const isRoutedConfirmation = id && pendingRoutedConfirmation !== -1; + + const [currentPendingConfirmation, setCurrentPendingConfirmation] = useState( + // Confirmations that are directly routed to get priority and will be initially shown above the current queue. + isRoutedConfirmation ? pendingRoutedConfirmation : 0, + ); + + const pendingConfirmation = pendingConfirmations[currentPendingConfirmation]; const [matchedChain, setMatchedChain] = useState({}); const [chainFetchComplete, setChainFetchComplete] = useState(false); @@ -369,7 +373,8 @@ export default function ConfirmationPage({ try { if (useSafeChainsListValidation) { const response = await fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, functionName: 'getSafeChainsList', }); diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap index 28306720e577..0bd7028048ef 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-named-snap-account.test.js.snap @@ -99,103 +99,3 @@ exports[`create-named-snap-account confirmation matches snapshot 1`] = `
`; - -exports[`create-named-snap-account confirmation matches snapshot 2`] = ` -
-
-
- -
- -
-`; 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`] = ` >
`; -exports[`switch-ethereum-chain confirmation should match snapshot 2`] = `
`; - exports[`switch-ethereum-chain confirmation should show alert if there are pending txs 1`] = `
actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: !originIsMetaMask, }; diff --git a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js index c03d03d3891a..7dbdb8c9c757 100644 --- a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js +++ b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { JustifyContent, SEVERITIES, @@ -85,7 +85,7 @@ function getValues(pendingApproval, t, actions) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: true, }; diff --git a/ui/pages/confirmations/constants/index.ts b/ui/pages/confirmations/constants/index.ts index 38fd05b714ba..7e26ce5c6d62 100644 --- a/ui/pages/confirmations/constants/index.ts +++ b/ui/pages/confirmations/constants/index.ts @@ -9,3 +9,8 @@ export const TYPED_SIGNATURE_VERSIONS = { }; export const SPENDING_CAP_UNLIMITED_MSG = 'UNLIMITED MESSAGE'; + +export const TypedSignSignaturePrimaryTypes = { + PERMIT: 'Permit', + ORDER: 'Order', +}; diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts index 60531b5680d7..e8cfc802ee99 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.test.ts @@ -142,7 +142,7 @@ describe('useInsufficientBalanceAlerts', () => { 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/alerts/useConfirmationOriginAlerts.ts b/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts index f84ba991f071..2f2af8ddf0de 100644 --- a/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/useConfirmationOriginAlerts.ts @@ -20,7 +20,7 @@ const useConfirmationOriginAlerts = (): Alert[] => { : (currentConfirmation as TransactionMeta)?.origin; const originUndefinedOrValid = - origin === undefined || isValidASCIIURL(origin); + origin === undefined || origin === 'metamask' || isValidASCIIURL(origin); return useMemo((): Alert[] => { if (originUndefinedOrValid) { 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/useGetTokenStandardAndDetails.test.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts new file mode 100644 index 000000000000..7cd217db3a85 --- /dev/null +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.test.ts @@ -0,0 +1,50 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + +import * as TokenActions from '../utils/token'; +import { useGetTokenStandardAndDetails } from './useGetTokenStandardAndDetails'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: () => 0x1, +})); + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + +jest.mock('../../../store/actions', () => { + return { + getTokenStandardAndDetails: jest + .fn() + .mockResolvedValue({ decimals: 2, standard: 'ERC20' }), + }; +}); + +describe('useGetTokenStandardAndDetails', () => { + it('should return token details', () => { + const { result } = renderHook(() => useGetTokenStandardAndDetails('0x5')); + expect(result.current).toEqual({ decimalsNumber: undefined }); + }); + + it('should return token details obtained from getTokenStandardAndDetails action', async () => { + jest + .spyOn(TokenActions, 'memoizedGetTokenStandardAndDetails') + .mockResolvedValue({ + standard: 'ERC20', + } as TokenActions.TokenDetailsERC20); + const { result, rerender } = renderHook(() => + useGetTokenStandardAndDetails('0x5'), + ); + + rerender(); + + await waitFor(() => { + expect(result.current).toEqual({ + decimalsNumber: 18, + standard: 'ERC20', + }); + }); + }); +}); diff --git a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts new file mode 100644 index 000000000000..88dfb0a12b9d --- /dev/null +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts @@ -0,0 +1,42 @@ +import { Hex } from '@metamask/utils'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { useAsyncResult } from '../../../hooks/useAsyncResult'; +import { + ERC20_DEFAULT_DECIMALS, + parseTokenDetailDecimals, + memoizedGetTokenStandardAndDetails, + TokenDetailsERC20, +} from '../utils/token'; + +/** + * Returns token details for a given token contract + * + * @param tokenAddress + * @returns + */ +export const useGetTokenStandardAndDetails = ( + tokenAddress: Hex | string | undefined, +) => { + const { value: details } = useAsyncResult( + async () => + (await memoizedGetTokenStandardAndDetails( + tokenAddress, + )) as TokenDetailsERC20, + [tokenAddress], + ); + + if (!details) { + return { decimalsNumber: undefined }; + } + + const { decimals, standard } = details || {}; + + if (standard === TokenStandard.ERC20) { + const parsedDecimals = + parseTokenDetailDecimals(decimals) ?? ERC20_DEFAULT_DECIMALS; + details.decimalsNumber = parsedDecimals; + } + + return details; +}; 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/hooks/useTrackERC20WithoutDecimalInformation.test.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts new file mode 100644 index 000000000000..dff0103fbe21 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts @@ -0,0 +1,40 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useContext } from 'react'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { TokenDetailsERC20 } from '../utils/token'; +import useTrackERC20WithoutDecimalInformation from './useTrackERC20WithoutDecimalInformation'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: () => 0x1, +})); + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + +describe('useTrackERC20WithoutDecimalInformation', () => { + const useContextMock = jest.mocked(useContext); + + const trackEventMock = jest.fn(); + + it('should invoke trackEvent method', () => { + useContextMock.mockImplementation((context) => { + if (context === MetaMetricsContext) { + return trackEventMock; + } + return undefined; + }); + + renderHook(() => + useTrackERC20WithoutDecimalInformation('0x5', { + standard: TokenStandard.ERC20, + } as TokenDetailsERC20), + ); + + expect(trackEventMock).toHaveBeenCalled(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts new file mode 100644 index 000000000000..fa6a5e620fc4 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts @@ -0,0 +1,58 @@ +import { useSelector } from 'react-redux'; +import { useContext, useEffect } from 'react'; +import { Hex } from '@metamask/utils'; + +import { + MetaMetricsEventCategory, + MetaMetricsEventLocation, + MetaMetricsEventName, + MetaMetricsEventUiCustomization, +} from '../../../../shared/constants/metametrics'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { getCurrentChainId } from '../../../selectors'; +import { parseTokenDetailDecimals, TokenDetailsERC20 } from '../utils/token'; + +/** + * Track event that number of decimals in ERC20 is not obtained + * + * @param tokenAddress + * @param tokenDetails + * @param metricLocation + */ +const useTrackERC20WithoutDecimalInformation = ( + tokenAddress: Hex | string | undefined, + tokenDetails?: TokenDetailsERC20, + metricLocation = MetaMetricsEventLocation.SignatureConfirmation, +) => { + const trackEvent = useContext(MetaMetricsContext); + const chainId = useSelector(getCurrentChainId); + + useEffect(() => { + if (chainId === undefined || tokenDetails === undefined) { + return; + } + const { decimals, standard } = tokenDetails || {}; + if (standard === TokenStandard.ERC20) { + const parsedDecimals = parseTokenDetailDecimals(decimals); + if (parsedDecimals === undefined) { + trackEvent({ + event: MetaMetricsEventName.SimulationIncompleteAssetDisplayed, + category: MetaMetricsEventCategory.Confirmations, + properties: { + token_decimals_available: false, + asset_address: tokenAddress, + asset_type: TokenStandard.ERC20, + chain_id: chainId, + location: metricLocation, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + }, + }); + } + } + }, [tokenDetails, chainId, tokenAddress, trackEvent]); +}; + +export default useTrackERC20WithoutDecimalInformation; diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js new file mode 100644 index 000000000000..38468749782d --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js @@ -0,0 +1,27 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { permitNFTSignatureMsg } from '../../../../test/data/confirmations/typed_sign'; +import { unapprovedPersonalSignMsg } from '../../../../test/data/confirmations/personal_sign'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; +import { useTypedSignSignatureInfo } from './useTypedSignSignatureInfo'; + +describe('useTypedSignSignatureInfo', () => { + it('should return details for primaty type and token standard', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(permitNFTSignatureMsg), + ); + expect(result.current.primaryType).toStrictEqual( + TypedSignSignaturePrimaryTypes.PERMIT, + ); + expect(result.current.tokenStandard).toStrictEqual(TokenStandard.ERC721); + }); + + it('should return empty object if confirmation is not typed sign', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(unapprovedPersonalSignMsg), + ); + expect(result.current.primaryType).toBeUndefined(); + expect(result.current.tokenStandard).toBeUndefined(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts new file mode 100644 index 000000000000..30d4e58f1525 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; + +import { + isOrderSignatureRequest, + isPermitSignatureRequest, + isSignatureTransactionType, +} from '../utils'; +import { SignatureRequestType } from '../types/confirm'; +import { parseTypedDataMessage } from '../../../../shared/modules/transaction.utils'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; + +export const useTypedSignSignatureInfo = ( + confirmation: SignatureRequestType, +) => { + const primaryType = useMemo(() => { + if ( + !confirmation || + !isSignatureTransactionType(confirmation) || + confirmation?.type !== MESSAGE_TYPE.ETH_SIGN_TYPED_DATA + ) { + return undefined; + } + if (isPermitSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.PERMIT; + } else if (isOrderSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.ORDER; + } + return undefined; + }, [confirmation]); + + // here we are using presence of tokenId in typed message data to know if its NFT permit + // we can get contract details for verifyingContract but that is async process taking longer + // and result in confirmation page content loading late + const tokenStandard = useMemo(() => { + if (primaryType !== TypedSignSignaturePrimaryTypes.PERMIT) { + return undefined; + } + const { + message: { tokenId }, + } = parseTypedDataMessage(confirmation?.msgParams?.data as string); + if (tokenId !== undefined) { + return TokenStandard.ERC721; + } + return undefined; + }, [confirmation, primaryType]); + + return { + primaryType: primaryType as keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard, + }; +}; 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..a33c1dae735c 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, + TransactionType.tokenMethodTransfer, ]; export const REDESIGN_DEV_TRANSACTION_TYPES = [ ...REDESIGN_USER_TRANSACTION_TYPES, - TransactionType.tokenMethodApprove, - TransactionType.tokenMethodIncreaseAllowance, - TransactionType.tokenMethodSetApprovalForAll, ]; const SIGNATURE_APPROVAL_TYPES = [ diff --git a/ui/pages/confirmations/utils/token.test.ts b/ui/pages/confirmations/utils/token.test.ts index e71813713d79..250bff90c07c 100644 --- a/ui/pages/confirmations/utils/token.test.ts +++ b/ui/pages/confirmations/utils/token.test.ts @@ -1,6 +1,9 @@ import { getTokenStandardAndDetails } from '../../../store/actions'; import { ERC20_DEFAULT_DECIMALS } from '../constants/token'; -import { fetchErc20Decimals } from './token'; +import { + fetchErc20Decimals, + memoizedGetTokenStandardAndDetails, +} from './token'; const MOCK_ADDRESS = '0x514910771af9ca656af840dff83e8264ecf986ca'; const MOCK_DECIMALS = 36; @@ -14,7 +17,7 @@ describe('fetchErc20Decimals', () => { jest.clearAllMocks(); /** Reset memoized function using getTokenStandardAndDetails for each test */ - fetchErc20Decimals?.cache?.clear?.(); + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); }); it(`should return the default number, ${ERC20_DEFAULT_DECIMALS}, if no decimals were found from details`, async () => { diff --git a/ui/pages/confirmations/utils/token.ts b/ui/pages/confirmations/utils/token.ts index 1f94280129a9..3a8c3a2a671e 100644 --- a/ui/pages/confirmations/utils/token.ts +++ b/ui/pages/confirmations/utils/token.ts @@ -1,32 +1,89 @@ import { memoize } from 'lodash'; import { Hex } from '@metamask/utils'; +import { AssetsContractController } from '@metamask/assets-controllers'; import { getTokenStandardAndDetails } from '../../../store/actions'; +export type TokenDetailsERC20 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +> & { decimalsNumber: number }; + +export type TokenDetailsERC721 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +>; + +export type TokenDetailsERC1155 = Awaited< + ReturnType< + ReturnType['getDetails'] + > +>; + +export type TokenDetails = + | TokenDetailsERC20 + | TokenDetailsERC721 + | TokenDetailsERC1155; + export const ERC20_DEFAULT_DECIMALS = 18; -/** - * Fetches the decimals for the given token address. - * - * @param {Hex | string} address - The ethereum token contract address. It is expected to be in hex format. - * We currently accept strings since we have a patch that accepts a custom string - * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch} - */ -export const fetchErc20Decimals = memoize( - async (address: Hex | string): Promise => { +export const parseTokenDetailDecimals = ( + decStr?: string, +): number | undefined => { + if (!decStr) { + return undefined; + } + + for (const radix of [10, 16]) { + const parsedDec = parseInt(decStr, radix); + if (isFinite(parsedDec)) { + return parsedDec; + } + } + return undefined; +}; + +export const memoizedGetTokenStandardAndDetails = memoize( + async ( + tokenAddress?: Hex | string, + userAddress?: string, + tokenId?: string, + ): Promise> => { try { - const { decimals: decStr } = await getTokenStandardAndDetails(address); - if (!decStr) { - return ERC20_DEFAULT_DECIMALS; - } - for (const radix of [10, 16]) { - const parsedDec = parseInt(decStr, radix); - if (isFinite(parsedDec)) { - return parsedDec; - } + if (!tokenAddress) { + return {}; } - return ERC20_DEFAULT_DECIMALS; + + return (await getTokenStandardAndDetails( + tokenAddress, + userAddress, + tokenId, + )) as TokenDetails; } catch { - return ERC20_DEFAULT_DECIMALS; + return {}; } }, ); + +/** + * Fetches the decimals for the given token address. + * + * @param address - The ethereum token contract address. It is expected to be in hex format. + * We currently accept strings since we have a patch that accepts a custom string + * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch} + */ +export const fetchErc20Decimals = async ( + address: Hex | string, +): Promise => { + try { + const { decimals: decStr } = (await memoizedGetTokenStandardAndDetails( + address, + )) as TokenDetailsERC20; + const decimals = parseTokenDetailDecimals(decStr); + + return decimals ?? ERC20_DEFAULT_DECIMALS; + } catch { + return ERC20_DEFAULT_DECIMALS; + } +}; diff --git a/ui/pages/create-account/connect-hardware/index.js b/ui/pages/create-account/connect-hardware/index.js index 2884dacb77b6..85464baccb69 100644 --- a/ui/pages/create-account/connect-hardware/index.js +++ b/ui/pages/create-account/connect-hardware/index.js @@ -28,8 +28,8 @@ import { } from '../../../components/component-library'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { TextColor } from '../../../helpers/constants/design-system'; -import SelectHardware from './select-hardware'; import AccountList from './account-list'; +import SelectHardware from './select-hardware'; const U2F_ERROR = 'U2F'; const LEDGER_ERRORS_CODES = { @@ -277,7 +277,7 @@ class ConnectHardwareForm extends Component { }); }; - onUnlockAccounts = (device, path) => { + onUnlockAccounts = async (device, path) => { const { history, mostRecentOverviewPage, unlockHardwareWalletAccounts } = this.props; const { selectedAccounts } = this.state; @@ -290,6 +290,13 @@ class ConnectHardwareForm extends Component { MEW_PATH === path ? this.context.t('hardwareWalletLegacyDescription') : ''; + + // Get preferred device name for metrics. + const metricDeviceName = await this.props.getDeviceNameForMetric( + device, + path, + ); + return unlockHardwareWalletAccounts( selectedAccounts, device, @@ -302,7 +309,7 @@ class ConnectHardwareForm extends Component { event: MetaMetricsEventName.AccountAdded, properties: { account_type: MetaMetricsEventAccountType.Hardware, - account_hardware_type: device, + account_hardware_type: metricDeviceName, }, }); history.push(mostRecentOverviewPage); @@ -313,7 +320,7 @@ class ConnectHardwareForm extends Component { event: MetaMetricsEventName.AccountAddFailed, properties: { account_type: MetaMetricsEventAccountType.Hardware, - account_hardware_type: device, + account_hardware_type: metricDeviceName, error: e.message, }, }); @@ -439,6 +446,7 @@ class ConnectHardwareForm extends Component { ConnectHardwareForm.propTypes = { connectHardware: PropTypes.func, checkHardwareStatus: PropTypes.func, + getDeviceNameForMetric: PropTypes.func, forgetDevice: PropTypes.func, showAlert: PropTypes.func, hideAlert: PropTypes.func, @@ -472,6 +480,9 @@ const mapDispatchToProps = (dispatch) => { connectHardware: (deviceName, page, hdPath, t) => { return dispatch(actions.connectHardware(deviceName, page, hdPath, t)); }, + getDeviceNameForMetric: (deviceName, hdPath) => { + return dispatch(actions.getDeviceNameForMetric(deviceName, hdPath)); + }, checkHardwareStatus: (deviceName, hdPath) => { return dispatch(actions.checkHardwareStatus(deviceName, hdPath)); }, diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx index 6e0d2627d4aa..3f7782c1416d 100644 --- a/ui/pages/create-account/connect-hardware/index.test.tsx +++ b/ui/pages/create-account/connect-hardware/index.test.tsx @@ -13,10 +13,12 @@ import ConnectHardwareForm from '.'; const mockConnectHardware = jest.fn(); const mockCheckHardwareStatus = jest.fn().mockResolvedValue(false); +const mockGetgetDeviceNameForMetric = jest.fn().mockResolvedValue('ledger'); jest.mock('../../../store/actions', () => ({ connectHardware: () => mockConnectHardware, checkHardwareStatus: () => mockCheckHardwareStatus, + getDeviceNameForMetric: () => mockGetgetDeviceNameForMetric, })); jest.mock('../../../selectors', () => ({ @@ -30,6 +32,10 @@ jest.mock('../../../selectors', () => ({ }, })); +jest.mock('../../../ducks/bridge/selectors', () => ({ + getAllBridgeableNetworks: () => [], +})); + const MOCK_RECENT_PAGE = '/home'; jest.mock('../../../ducks/history/history', () => ({ getMostRecentOverviewPage: jest diff --git a/ui/pages/error/error.component.js b/ui/pages/error/error.component.js index 57a8e40c6473..f7ab9c593d40 100644 --- a/ui/pages/error/error.component.js +++ b/ui/pages/error/error.component.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; +import { getErrorMessage } from '../../../shared/modules/error'; import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; import { MetaMetricsContextProp, @@ -72,6 +73,7 @@ class ErrorPage extends PureComponent { const message = isPopup ? t('errorPagePopupMessage', [supportLink]) : t('errorPageMessage', [supportLink]); + const errorMessage = getErrorMessage(error); return (
@@ -81,8 +83,8 @@ class ErrorPage extends PureComponent {
{t('errorDetails')}
    - {error.message - ? this.renderErrorDetail(t('errorMessage', [error.message])) + {errorMessage + ? this.renderErrorDetail(t('errorMessage', [errorMessage])) : null} {error.code ? this.renderErrorDetail(t('errorCode', [error.code])) diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 2df3f2907266..37c147427ac5 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -62,8 +62,7 @@ import { CONNECTED_ROUTE, CONNECTED_ACCOUNTS_ROUTE, AWAITING_SWAP_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, CONFIRMATION_V_NEXT_ROUTE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -328,10 +327,8 @@ export default class Home extends PureComponent { const canRedirect = !isNotification && !stayOnHomePage; if (canRedirect && showAwaitingSwapScreen) { history.push(AWAITING_SWAP_ROUTE); - } else if (canRedirect && haveSwapsQuotes) { - history.push(VIEW_QUOTE_ROUTE); - } else if (canRedirect && swapsFetchParams) { - history.push(BUILD_QUOTE_ROUTE); + } else if (canRedirect && (haveSwapsQuotes || swapsFetchParams)) { + history.push(PREPARE_SWAP_ROUTE); } else if (firstPermissionsRequestId) { history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`); } else if (pendingConfirmationsPrioritized.length > 0) { diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 42bdfc685779..dfeb1a5e7cdb 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -51,7 +51,6 @@ import { getAccountType, ///: END:ONLY_INCLUDE_IF } from '../../selectors'; -import { getIsSmartTransactionsOptInModalAvailable } from '../../../shared/modules/selectors'; import { closeNotificationPopup, @@ -223,8 +222,10 @@ const mapStateToProps = (state) => { custodianDeepLink: getCustodianDeepLink(state), accountType: getAccountType(state), ///: END:ONLY_INCLUDE_IF - isSmartTransactionsOptInModalAvailable: - getIsSmartTransactionsOptInModalAvailable(state), + + // Set to false to prevent the opt-in modal from showing. + // TODO(dbrans): Remove opt-in modal once default opt-in is stable. + isSmartTransactionsOptInModalAvailable: false, showMultiRpcModal: state.metamask.preferences.showMultiRpcModal, }; }; 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/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap b/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap index ca6b1f7219b6..59120196cbdf 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap +++ b/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.tsx.snap @@ -55,57 +55,3 @@ exports[`Interactive Replacement Token Page should reject if there are errors 1`
`; - -exports[`Interactive Replacement Token Page should reject if there are errors 2`] = ` -
-
-
-
- Replace custodian token - - failed -
-
-
-
-

- Please go to displayName and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again. -

-
-
-
-
- - -
-
-
-
-`; diff --git a/ui/pages/keychains/reveal-seed.js b/ui/pages/keychains/reveal-seed.js index 492f37545138..cf3e285eba64 100644 --- a/ui/pages/keychains/reveal-seed.js +++ b/ui/pages/keychains/reveal-seed.js @@ -2,6 +2,7 @@ import qrCode from 'qrcode-generator'; import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { getErrorMessage } from '../../../shared/modules/error'; import { MetaMetricsEventCategory, MetaMetricsEventKeyType, @@ -97,7 +98,7 @@ export default function RevealSeedPage() { reason: e.message, // 'incorrect_password', }, }); - setError(e.message); + setError(getErrorMessage(e)); }); }; 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..9438f3859ff1 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 privacy settings" button is clicked', () => { const { getByText } = renderWithProvider(, store); - const privacySettingsButton = getByText('Advanced configuration'); + const privacySettingsButton = getByText('Manage default privacy 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..aed08f196957 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -1,10 +1,12 @@ import React, { useContext, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import classnames from 'classnames'; 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 +21,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 +36,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,23 +54,18 @@ import { getNetworkConfigurationsByChainId, } from '../../../selectors'; import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; -import { selectParticipateInMetaMetrics } from '../../../selectors/metamask-notifications/authentication'; import { - setCompletedOnboarding, setIpfsGateway, setUseCurrencyRateCheck, setUseMultiAccountBalanceChecker, - setUsePhishDetect, setUse4ByteResolution, setUseTokenDetection, setUseAddressBarEnsResolution, showModal, toggleNetworkMenu, setIncomingTransactionsPreferences, - toggleExternalServices, setUseTransactionSimulations, setPetnamesEnabled, - performSignIn, setEditedNetwork, } from '../../../store/actions'; import { @@ -80,6 +79,8 @@ import { } from '../../../../shared/constants/network'; import { Setting } from './setting'; +const ANIMATION_TIME = 500; + /** * Profile Syncing Setting props * @@ -116,6 +117,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,9 +133,7 @@ export default function PrivacySettings() { useTransactionSimulations, } = defaultState; const petnamesEnabled = useSelector(getPetnamesEnabled); - const participateInMetaMetrics = useSelector(selectParticipateInMetaMetrics); - const [usePhishingDetection, setUsePhishingDetection] = useState(null); const [turnOn4ByteResolution, setTurnOn4ByteResolution] = useState(use4ByteResolution); const [turnOnTokenDetection, setTurnOnTokenDetection] = @@ -158,38 +161,23 @@ export default function PrivacySettings() { getExternalServicesOnboardingToggleState, ); - const phishingToggleState = - usePhishingDetection === null - ? externalServicesOnboardingToggleState - : usePhishingDetection; - const profileSyncingProps = useProfileSyncingProps( externalServicesOnboardingToggleState, ); const handleSubmit = () => { - dispatch(toggleExternalServices(externalServicesOnboardingToggleState)); - dispatch(setUsePhishDetect(phishingToggleState)); dispatch(setUse4ByteResolution(turnOn4ByteResolution)); dispatch(setUseTokenDetection(turnOnTokenDetection)); dispatch( 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); } @@ -206,12 +194,11 @@ export default function PrivacySettings() { is_profile_syncing_enabled: profileSyncingProps.isProfileSyncingEnabled, is_basic_functionality_enabled: externalServicesOnboardingToggleState, show_incoming_tx: incomingTransactionsPreferences, - use_phising_detection: usePhishingDetection, turnon_token_detection: turnOnTokenDetection, }, }); - history.push(ONBOARDING_PIN_EXTENSION_ROUTE); + history.push(ONBOARDING_COMPLETION_ROUTE); }; const handleUseProfileSync = async () => { @@ -242,352 +229,510 @@ export default function PrivacySettings() { } }; + const handleItemSelected = (item) => { + setSelectedItem(item); + setShowDetail(true); + + setTimeout(() => { + setHiddenClass(false); + }, ANIMATION_TIME); + }; + + const handleBack = () => { + setShowDetail(false); + setTimeout(() => { + setHiddenClass(true); + }, ANIMATION_TIME); + }; + + 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..81943ee71f49 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js @@ -42,7 +42,6 @@ describe('Privacy Settings Onboarding View', () => { [CHAIN_IDS.LINEA_GOERLI]: true, [CHAIN_IDS.LINEA_SEPOLIA]: true, }, - usePhishDetect: true, use4ByteResolution: true, useTokenDetection: false, useCurrencyRateCheck: true, @@ -59,7 +58,6 @@ describe('Privacy Settings Onboarding View', () => { const store = configureMockStore([thunk])(mockStore); const setFeatureFlagStub = jest.fn(); - const setUsePhishDetectStub = jest.fn(); const setUse4ByteResolutionStub = jest.fn(); const setUseTokenDetectionStub = jest.fn(); const setUseCurrencyRateCheckStub = jest.fn(); @@ -79,7 +77,6 @@ describe('Privacy Settings Onboarding View', () => { setBackgroundConnection({ setFeatureFlag: setFeatureFlagStub, - setUsePhishDetect: setUsePhishDetectStub, setUse4ByteResolution: setUse4ByteResolutionStub, setUseTokenDetection: setUseTokenDetectionStub, setUseCurrencyRateCheck: setUseCurrencyRateCheckStub, @@ -97,14 +94,13 @@ 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, ); // All settings are initialized toggled to be same as default expect(toggleExternalServicesStub).toHaveBeenCalledTimes(0); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(0); expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(0); expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(0); expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(0); @@ -114,61 +110,73 @@ 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]); // setUse4ByteResolutionStub + fireEvent.click(toggles[1]); // 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(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 +190,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 +205,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 +217,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 +240,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/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js index 18208977fb90..8c37094dcbe4 100644 --- a/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js +++ b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.test.js @@ -1,4 +1,4 @@ -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, act } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -141,14 +141,16 @@ describe('Confirm Recovery Phrase Component', () => { 'recovery-phrase-confirm', ); - await waitFor(() => { + expect(confirmRecoveryPhraseButton).toBeDisabled(); + + act(() => { clock.advanceTimersByTime(500); // Wait for debounce + }); - expect(confirmRecoveryPhraseButton).not.toBeDisabled(); + expect(confirmRecoveryPhraseButton).not.toBeDisabled(); - fireEvent.click(confirmRecoveryPhraseButton); + fireEvent.click(confirmRecoveryPhraseButton); - expect(setSeedPhraseBackedUp).toHaveBeenCalledWith(true); - }); + expect(setSeedPhraseBackedUp).toHaveBeenCalledWith(true); }); }); diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 2578286032d2..378622a3994a 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -28,5 +28,5 @@ @import 'create-snap-account/index'; @import 'remove-snap-account/index'; @import 'swaps/index'; -@import 'token-details/index'; +@import 'bridge/index'; @import 'unlock-page/index'; 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..ad53f67a7127 --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap @@ -0,0 +1,491 @@ +// 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 +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+`; + +exports[`ConnectPage should render with defaults from the requested permissions 1`] = ` +
+
+
+
+
+

+

+ Connect with MetaMask +

+

+ This site wants to + : +

+

+
+
+
+
+
+
+ +
+
+

+ See your accounts and suggest transactions +

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

+ Use your enabled networks +

+
+ + Requesting for + +
Alerts"" + data-tooltipped="" + style="display: inline;" + > +
+
+
+
+ Custom Mainnet RPC logo +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+`; diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx new file mode 100644 index 000000000000..ef705e474ad9 --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; +import { ConnectPage, ConnectPageRequest } from './connect-page'; + +const render = ( + 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(); + }); + + it('should render with defaults from the requested permissions', () => { + const { container } = render({ + request: { + id: '1', + origin: 'https://test.dapp', + permissions: { + [RestrictedMethods.eth_accounts]: { + caveats: [ + { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], + }, + ], + }, + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + permissionsRequestId: '1', + rejectPermissionsRequest: jest.fn(), + approveConnection: jest.fn(), + activeTabOrigin: 'https://test.dapp', + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx new file mode 100644 index 000000000000..0ae22b3d9e0f --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -0,0 +1,186 @@ +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'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; + +export type ConnectPageRequest = { + id: string; + origin: string; + permissions?: Record< + string, + { caveats?: { type: string; value: string[] }[] } + >; +}; + +type ConnectPageProps = { + request: ConnectPageRequest; + permissionsRequestId: string; + rejectPermissionsRequest: (id: string) => void; + approveConnection: (request: ConnectPageRequest) => void; + activeTabOrigin: string; +}; + +export const ConnectPage: React.FC = ({ + request, + permissionsRequestId, + rejectPermissionsRequest, + approveConnection, +}) => { + const t = useI18nContext(); + + const ethAccountsPermission = + request?.permissions?.[RestrictedMethods.eth_accounts]; + const requestedAccounts = + ethAccountsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, + )?.value || []; + + const permittedChainsPermission = + request?.permissions?.[EndowmentTypes.permittedChains]; + const requestedChainIds = + permittedChainsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value || []; + + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const [nonTestNetworks, testNetworks] = useMemo( + () => + 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 = + requestedChainIds.length > 0 + ? requestedChainIds + : 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 = + requestedAccounts.length > 0 + ? requestedAccounts + : [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..e32f85609406 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -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) } @@ -423,7 +443,7 @@ export default class PermissionConnect extends Component { rejectSnapInstall={(requestId) => { rejectPendingApproval( requestId, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); this.setState({ permissionsApproved: true }); }} @@ -449,7 +469,7 @@ export default class PermissionConnect extends Component { rejectSnapUpdate={(requestId) => { rejectPendingApproval( requestId, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); this.setState({ permissionsApproved: false }); }} 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..d59c4b29e0c1 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,11 +38,11 @@ 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'; import OnboardingAppHeader from '../onboarding-flow/onboarding-app-header/onboarding-app-header'; -import TokenDetailsPage from '../token-details'; import Notifications from '../notifications'; import NotificationsSettings from '../notifications-settings'; import NotificationDetails from '../notification-details'; @@ -70,13 +71,12 @@ import { SWAPS_ROUTE, SETTINGS_ROUTE, UNLOCK_ROUTE, - BUILD_QUOTE_ROUTE, CONFIRMATION_V_NEXT_ROUTE, ONBOARDING_ROUTE, ONBOARDING_UNLOCK_ROUTE, - TOKEN_DETAILS, CONNECTIONS, PERMISSIONS, + REVIEW_PERMISSIONS, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) INSTITUTIONAL_FEATURES_DONE_ROUTE, CUSTODY_ACCOUNT_DONE_ROUTE, @@ -189,6 +189,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 +201,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, @@ -363,11 +366,6 @@ export default class Routes extends Component { component={ConfirmTransaction} /> - + ); @@ -489,13 +492,6 @@ export default class Routes extends Component { ); } - onSwapsBuildQuotePage() { - const { location } = this.props; - return Boolean( - matchPath(location.pathname, { path: BUILD_QUOTE_ROUTE, exact: false }), - ); - } - onHomeScreen() { const { location } = this.props; return location.pathname === DEFAULT_ROUTE; @@ -504,6 +500,17 @@ export default class Routes extends Component { hideAppHeader() { const { location } = this.props; + const isCrossChainSwapsPage = Boolean( + matchPath(location.pathname, { + path: `${CROSS_CHAIN_SWAP_ROUTE}`, + exact: false, + }), + ); + + if (isCrossChainSwapsPage) { + return true; + } + const isNotificationsPage = Boolean( matchPath(location.pathname, { path: `${NOTIFICATIONS_ROUTE}`, @@ -554,6 +561,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 +653,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 +673,7 @@ export default class Routes extends Component { return ( + {showConnectAccountToast && !this.state.hideConnectAccountToast && isEvmAccount ? ( @@ -735,7 +756,7 @@ export default class Routes extends Component { } @@ -761,6 +782,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 +976,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..2c26f0daa0b5 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -28,8 +28,8 @@ import { getUseRequestQueue, getUseNftDetection, getNftDetectionEnablementToast, + getCurrentNetwork, } from '../../selectors'; -import { getSmartTransactionsOptInStatus } from '../../../shared/modules/selectors'; import { lockMetamask, hideImportNftsModal, @@ -52,6 +52,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 +78,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 && @@ -115,7 +117,6 @@ function mapStateToProps(state) { allAccountsOnNetworkAreEmpty: getAllAccountsOnNetworkAreEmpty(state), isTestNet: getIsTestnet(state), showExtensionInFullSizeView: getShowExtensionInFullSizeView(state), - smartTransactionsOptInStatus: getSmartTransactionsOptInStatus(state), currentChainId: getCurrentChainId(state), shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state), forgottenPassword: state.metamask.forgottenPassword, @@ -129,10 +130,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 +163,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 newValue = !oldValue; - setSmartTransactionsOptInStatus(newValue); + setSmartTransactionsEnabled(newValue); }} offLabel={t('off')} onLabel={t('on')} diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js index 6be53fb4e21a..2c64b79e4f4d 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js @@ -9,7 +9,7 @@ import AdvancedTab from '.'; const mockSetAutoLockTimeLimit = jest.fn().mockReturnValue({ type: 'TYPE' }); const mockSetShowTestNetworks = jest.fn(); const mockSetShowFiatConversionOnTestnetsPreference = jest.fn(); -const mockSetStxOptIn = jest.fn(); +const mockSetStxPrefEnabled = jest.fn(); jest.mock('../../../store/actions.ts', () => { return { @@ -17,7 +17,7 @@ jest.mock('../../../store/actions.ts', () => { setShowTestNetworks: () => mockSetShowTestNetworks, setShowFiatConversionOnTestnetsPreference: () => mockSetShowFiatConversionOnTestnetsPreference, - setSmartTransactionsOptInStatus: () => mockSetStxOptIn, + setSmartTransactionsPreferenceEnabled: () => mockSetStxPrefEnabled, }; }); @@ -102,7 +102,7 @@ describe('AdvancedTab Component', () => { const { queryByTestId } = renderWithProvider(, mockStore); const toggleButton = queryByTestId('settings-page-stx-opt-in-toggle'); fireEvent.click(toggleButton); - expect(mockSetStxOptIn).toHaveBeenCalled(); + expect(mockSetStxPrefEnabled).toHaveBeenCalled(); }); }); }); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index 2a11b6751dae..f2ad894d1e8b 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -12,10 +12,11 @@ import { setShowExtensionInFullSizeView, setShowFiatConversionOnTestnetsPreference, setShowTestNetworks, - setSmartTransactionsOptInStatus, + setSmartTransactionsPreferenceEnabled, setUseNonceField, showModal, } from '../../../store/actions'; +import { getSmartTransactionsPreferenceEnabled } from '../../../../shared/modules/selectors'; import AdvancedTab from './advanced-tab.component'; export const mapStateToProps = (state) => { @@ -32,7 +33,6 @@ export const mapStateToProps = (state) => { showFiatInTestnets, showTestNetworks, showExtensionInFullSizeView, - smartTransactionsOptInStatus, autoLockTimeLimit = DEFAULT_AUTO_LOCK_TIME_LIMIT, } = getPreferences(state); @@ -42,7 +42,7 @@ export const mapStateToProps = (state) => { showFiatInTestnets, showTestNetworks, showExtensionInFullSizeView, - smartTransactionsOptInStatus, + smartTransactionsEnabled: getSmartTransactionsPreferenceEnabled(state), autoLockTimeLimit, useNonceField, dismissSeedBackUpReminder, @@ -67,8 +67,8 @@ export const mapDispatchToProps = (dispatch) => { setShowExtensionInFullSizeView: (value) => { return dispatch(setShowExtensionInFullSizeView(value)); }, - setSmartTransactionsOptInStatus: (value) => { - return dispatch(setSmartTransactionsOptInStatus(value)); + setSmartTransactionsEnabled: (value) => { + return dispatch(setSmartTransactionsPreferenceEnabled(value)); }, setAutoLockTimeLimit: (value) => { return dispatch(setAutoLockTimeLimit(value)); diff --git a/ui/pages/settings/alerts-tab/alerts-tab.js b/ui/pages/settings/alerts-tab/alerts-tab.js deleted file mode 100644 index 7523c68f6fa1..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; - -import { AlertTypes } from '../../../../shared/constants/alerts'; -import Tooltip from '../../../components/ui/tooltip'; -import ToggleButton from '../../../components/ui/toggle-button'; -import { setAlertEnabledness } from '../../../store/actions'; -import { getAlertEnabledness } from '../../../ducks/metamask/metamask'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { handleSettingsRefs } from '../../../helpers/utils/settings-search'; -import { Icon, IconName } from '../../../components/component-library'; - -const AlertSettingsEntry = ({ alertId, description, title }) => { - 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/networks-tab/networks-form/use-safe-chains.ts b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts index 56b237e9fce4..3556c2196b31 100644 --- a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts +++ b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { useSafeChainsListValidationSelector } from '../../../../selectors'; import fetchWithCache from '../../../../../shared/lib/fetch-with-cache'; +import { CHAIN_SPEC_URL } from '../../../../../shared/constants/network'; import { DAY } from '../../../../../shared/constants/time'; export type SafeChain = { @@ -25,8 +26,9 @@ export const useSafeChains = () => { if (useSafeChainsListValidation) { useEffect(() => { fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, functionName: 'getSafeChainsList', + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, }) .then((response) => { 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/snaps/snap-view/snap-view.js b/ui/pages/snaps/snap-view/snap-view.js index 3cc5cd999047..b8849179cabd 100644 --- a/ui/pages/snaps/snap-view/snap-view.js +++ b/ui/pages/snaps/snap-view/snap-view.js @@ -105,12 +105,14 @@ function SnapView() { showInfo={false} startAccessory={renderBackButton()} endAccessory={ - <SnapHomeMenu - snapId={snapId} - onSettingsClick={handleSettingsClick} - onRemoveClick={handleSnapRemove} - isSettingsAvailable={!snap.preinstalled} - /> + !snap.hidden && ( + <SnapHomeMenu + snapId={snapId} + onSettingsClick={handleSettingsClick} + onRemoveClick={handleSnapRemove} + isSettingsAvailable={!snap.preinstalled} + /> + ) } /> )} diff --git a/ui/pages/swaps/__snapshots__/index.test.js.snap b/ui/pages/swaps/__snapshots__/index.test.js.snap index 779bb78555d5..c7a58c20dac8 100644 --- a/ui/pages/swaps/__snapshots__/index.test.js.snap +++ b/ui/pages/swaps/__snapshots__/index.test.js.snap @@ -12,17 +12,29 @@ exports[`Swap renders the component with initial props 1`] = ` class="swaps__header" > <div - class="swaps__header-edit" - /> + class="box box--margin-left-4 box--display-flex box--flex-direction-row box--justify-content-center box--width-1/12" + tabindex="0" + > + <span + class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/arrow-2-left.svg'); cursor: pointer;" + title="Cancel" + /> + </div> <div class="swaps__title" > Swap </div> <div - class="swaps__header-cancel" + class="box box--margin-right-4 box--display-flex box--flex-direction-row box--justify-content-center box--width-1/12" + tabindex="0" > - Cancel + <span + class="mm-box mm-icon mm-icon--size-lg mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/setting.svg'); cursor: pointer;" + title="Transaction settings" + /> </div> </div> <div diff --git a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js index 777e253a70fe..56db1578ce9a 100644 --- a/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js +++ b/ui/pages/swaps/awaiting-signatures/awaiting-signatures.js @@ -15,12 +15,12 @@ import { getHardwareWalletType, } from '../../../selectors/selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { DEFAULT_ROUTE, - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import PulseLoader from '../../../components/ui/pulse-loader'; import Box from '../../../components/ui/box'; @@ -47,7 +47,7 @@ export default function AwaitingSignatures() { const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( @@ -150,7 +150,7 @@ export default function AwaitingSignatures() { // Go to the default route and then to the build quote route in order to clean up // the `inputValue` local state in `pages/swaps/index.js` history.push(DEFAULT_ROUTE); - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); }} submitText={t('cancel')} hideCancel diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index 660f7ef4fcae..7c410ca03ce5 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -23,8 +23,8 @@ import { getFullTxData, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { @@ -33,7 +33,7 @@ import { getApproveTxParams, getUsedSwapsGasPrice, fetchQuotesAndSetQuoteState, - navigateBackToBuildQuote, + navigateBackToPrepareSwap, prepareForRetryGetQuotes, prepareToLeaveSwaps, getCurrentSmartTransactionsEnabled, @@ -120,7 +120,7 @@ export default function AwaitingSwap({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( @@ -318,7 +318,7 @@ export default function AwaitingSwap({ ), ); } else if (errorKey) { - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); } else if ( isSwapsDefaultTokenSymbol(destinationTokenSymbol, chainId) || swapComplete @@ -329,7 +329,9 @@ export default function AwaitingSwap({ history.push(DEFAULT_ROUTE); } }} - onCancel={async () => await dispatch(navigateBackToBuildQuote(history))} + onCancel={async () => + await dispatch(navigateBackToPrepareSwap(history)) + } submitText={submitText} disabled={submittingSwap} hideCancel={errorKey !== QUOTES_EXPIRED_ERROR} diff --git a/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap b/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap deleted file mode 100644 index b0551966d1c6..000000000000 --- a/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BuildQuote renders the component with initial props 1`] = ` -<div - class="button-group slippage-buttons__button-group radio-button-group" - role="radiogroup" -> - <button - aria-checked="true" - class="button-group__button radio-button button-group__button--active radio-button--active" - data-testid="button-group__button0" - role="radio" - > - 2% - </button> - <button - aria-checked="false" - class="button-group__button radio-button" - data-testid="button-group__button1" - role="radio" - > - 3% - </button> - <button - aria-checked="false" - class="button-group__button slippage-buttons__button-group-custom-button radio-button" - data-testid="button-group__button2" - role="radio" - > - custom - </button> -</div> -`; diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js deleted file mode 100644 index 23ca35b3f2e9..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.js +++ /dev/null @@ -1,800 +0,0 @@ -import React, { useContext, useEffect, useState, useCallback } from 'react'; -import BigNumber from 'bignumber.js'; -import PropTypes from 'prop-types'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import classnames from 'classnames'; -import { uniqBy, isEqual } from 'lodash'; -import { useHistory } from 'react-router-dom'; -import { getTokenTrackerLink } from '@metamask/etherscan-link'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - useTokensToSearch, - getRenderableTokenData, -} from '../../../hooks/useTokensToSearch'; -import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; -import { I18nContext } from '../../../contexts/i18n'; -import DropdownInputPair from '../dropdown-input-pair'; -import DropdownSearchList from '../dropdown-search-list'; -import SlippageButtons from '../slippage-buttons'; -import { getTokens, getConversionRate } from '../../../ducks/metamask/metamask'; -import InfoTooltip from '../../../components/ui/info-tooltip'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import { - VIEW_QUOTE_ROUTE, - LOADING_QUOTES_ROUTE, -} from '../../../helpers/constants/routes'; - -import { - fetchQuotesAndSetQuoteState, - setSwapsFromToken, - setSwapToToken, - getFromToken, - getToToken, - getBalanceError, - getTopAssets, - getFetchParams, - getQuotes, - setBalanceError, - setFromTokenInputValue, - setFromTokenError, - setMaxSlippage, - setReviewSwapClickedTimestamp, - getCurrentSmartTransactionsEnabled, - getFromTokenInputValue, - getFromTokenError, - getMaxSlippage, - getIsFeatureFlagLoaded, - getSmartTransactionFees, - getLatestAddedTokenTo, -} from '../../../ducks/swaps/swaps'; -import { - getSwapsDefaultToken, - getTokenExchangeRates, - getCurrentCurrency, - getCurrentChainId, - getRpcPrefsForCurrentProvider, - getTokenList, - isHardwareWallet, - getHardwareWalletType, - getUseCurrencyRateCheck, -} from '../../../selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; - -import { getURLHostName } from '../../../helpers/utils/util'; -import { usePrevious } from '../../../hooks/usePrevious'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; -import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; - -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from '../../../../shared/modules/swaps.utils'; -import { - MetaMetricsEventCategory, - MetaMetricsEventLinkType, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; -import { - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, - TokenBucketPriority, - MAX_ALLOWED_SLIPPAGE, -} from '../../../../shared/constants/swaps'; - -import { - resetSwapsPostFetchState, - ignoreTokens, - setBackgroundSwapRouteState, - clearSwapsQuotes, - stopPollingForQuotes, - clearSmartTransactionFees, -} from '../../../store/actions'; -import { countDecimals, fetchTokenPrice } from '../swaps.util'; -import SwapsFooter from '../swaps-footer'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; -import { fetchTokenBalance } from '../../../../shared/lib/token-util'; -import { shouldEnableDirectWrapping } from '../../../../shared/lib/swaps-utils'; -import { - getValueFromWeiHex, - hexToDecimal, -} from '../../../../shared/modules/conversion.utils'; - -const fuseSearchKeys = [ - { name: 'name', weight: 0.499 }, - { name: 'symbol', weight: 0.499 }, - { name: 'address', weight: 0.002 }, -]; - -let timeoutIdForQuotesPrefetching; - -export default function BuildQuote({ - ethBalance, - selectedAccountAddress, - shuffledTokensList, -}) { - const t = useContext(I18nContext); - const dispatch = useDispatch(); - const history = useHistory(); - const trackEvent = useContext(MetaMetricsContext); - - const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] = - useState(undefined); - const [verificationClicked, setVerificationClicked] = useState(false); - - const isFeatureFlagLoaded = useSelector(getIsFeatureFlagLoaded); - const balanceError = useSelector(getBalanceError); - const fetchParams = useSelector(getFetchParams, isEqual); - const { sourceTokenInfo = {}, destinationTokenInfo = {} } = - fetchParams?.metaData || {}; - const tokens = useSelector(getTokens, isEqual); - const topAssets = useSelector(getTopAssets, isEqual); - const fromToken = useSelector(getFromToken, isEqual); - const fromTokenInputValue = useSelector(getFromTokenInputValue); - const fromTokenError = useSelector(getFromTokenError); - const maxSlippage = useSelector(getMaxSlippage); - const toToken = useSelector(getToToken, isEqual) || destinationTokenInfo; - const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); - const tokenList = useSelector(getTokenList, isEqual); - const quotes = useSelector(getQuotes, isEqual); - const areQuotesPresent = Object.keys(quotes).length > 0; - const latestAddedTokenTo = useSelector(getLatestAddedTokenTo, isEqual); - - const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); - const conversionRate = useSelector(getConversionRate); - const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - const smartTransactionFees = useSelector(getSmartTransactionFees); - const currentCurrency = useSelector(getCurrentCurrency); - - const fetchParamsFromToken = isSwapsDefaultTokenSymbol( - sourceTokenInfo?.symbol, - chainId, - ) - ? defaultSwapsToken - : sourceTokenInfo; - - const { loading, tokensWithBalances } = useTokenTracker({ tokens }); - - // If the fromToken was set in a call to `onFromSelect` (see below), and that from token has a balance - // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that - // the balance of the token can appear in the from token selection dropdown - const fromTokenArray = - !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance - ? [fromToken] - : []; - const usersTokens = uniqBy( - [...tokensWithBalances, ...tokens, ...fromTokenArray], - 'address', - ); - const memoizedUsersTokens = useEqualityCheck(usersTokens); - - const selectedFromToken = getRenderableTokenData( - fromToken || fetchParamsFromToken, - tokenConversionRates, - conversionRate, - currentCurrency, - chainId, - tokenList, - ); - - const tokensToSearchSwapFrom = useTokensToSearch({ - usersTokens: memoizedUsersTokens, - topTokens: topAssets, - shuffledTokensList, - tokenBucketPriority: TokenBucketPriority.owned, - }); - const tokensToSearchSwapTo = useTokensToSearch({ - usersTokens: memoizedUsersTokens, - topTokens: topAssets, - shuffledTokensList, - tokenBucketPriority: TokenBucketPriority.top, - }); - const selectedToToken = - tokensToSearchSwapFrom.find(({ address }) => - isEqualCaseInsensitive(address, toToken?.address), - ) || toToken; - const toTokenIsNotDefault = - selectedToToken?.address && - !isSwapsDefaultTokenAddress(selectedToToken?.address, chainId); - const occurrences = Number( - selectedToToken?.occurances || selectedToToken?.occurrences || 0, - ); - const { - address: fromTokenAddress, - symbol: fromTokenSymbol, - string: fromTokenString, - decimals: fromTokenDecimals, - balance: rawFromTokenBalance, - } = selectedFromToken || {}; - const { address: toTokenAddress } = selectedToToken || {}; - - const fromTokenBalance = - rawFromTokenBalance && - calcTokenAmount(rawFromTokenBalance, fromTokenDecimals).toString(10); - - const prevFromTokenBalance = usePrevious(fromTokenBalance); - - const swapFromTokenFiatValue = useTokenFiatAmount( - fromTokenAddress, - fromTokenInputValue || 0, - fromTokenSymbol, - { - showFiat: useCurrencyRateCheck, - }, - true, - ); - const swapFromEthFiatValue = useEthFiatAmount( - fromTokenInputValue || 0, - { showFiat: useCurrencyRateCheck }, - true, - ); - const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) - ? swapFromEthFiatValue - : swapFromTokenFiatValue; - - const onInputChange = useCallback( - (newInputValue, balance) => { - dispatch(setFromTokenInputValue(newInputValue)); - const newBalanceError = new BigNumber(newInputValue || 0).gt( - balance || 0, - ); - // "setBalanceError" is just a warning, a user can still click on the "Review swap" button. - if (balanceError !== newBalanceError) { - dispatch(setBalanceError(newBalanceError)); - } - dispatch( - setFromTokenError( - fromToken && countDecimals(newInputValue) > fromToken.decimals - ? 'tooManyDecimals' - : null, - ), - ); - }, - [dispatch, fromToken, balanceError], - ); - - const onFromSelect = (token) => { - if ( - token?.address && - !swapFromFiatValue && - fetchedTokenExchangeRate !== null - ) { - fetchTokenPrice(token.address).then((rate) => { - if (rate !== null && rate !== undefined) { - setFetchedTokenExchangeRate(rate); - } - }); - } else { - setFetchedTokenExchangeRate(null); - } - if ( - token?.address && - !memoizedUsersTokens.find((usersToken) => - isEqualCaseInsensitive(usersToken.address, token.address), - ) - ) { - fetchTokenBalance( - token.address, - selectedAccountAddress, - global.ethereumProvider, - ).then((fetchedBalance) => { - if (fetchedBalance?.balance) { - const balanceAsDecString = fetchedBalance.balance.toString(10); - const userTokenBalance = calcTokenAmount( - balanceAsDecString, - token.decimals, - ); - dispatch( - setSwapsFromToken({ - ...token, - string: userTokenBalance.toString(10), - balance: balanceAsDecString, - }), - ); - } - }); - } - dispatch(setSwapsFromToken(token)); - onInputChange( - token?.address ? fromTokenInputValue : '', - token.string, - token.decimals, - ); - }; - - const blockExplorerTokenLink = getTokenTrackerLink( - selectedToToken.address, - chainId, - null, // no networkId - null, // no holderAddress - { - blockExplorerUrl: - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, - }, - ); - - const blockExplorerLabel = rpcPrefs.blockExplorerUrl - ? getURLHostName(blockExplorerTokenLink) - : t('etherscan'); - - const { address: toAddress } = toToken || {}; - const onToSelect = useCallback( - (token) => { - if (latestAddedTokenTo && token.address !== toAddress) { - dispatch( - ignoreTokens({ - tokensToIgnore: toAddress, - dontShowLoadingIndicator: true, - }), - ); - } - dispatch(setSwapToToken(token)); - setVerificationClicked(false); - }, - [dispatch, latestAddedTokenTo, toAddress], - ); - - const hideDropdownItemIf = useCallback( - (item) => isEqualCaseInsensitive(item.address, fromTokenAddress), - [fromTokenAddress], - ); - - const tokensWithBalancesFromToken = tokensWithBalances.find((token) => - isEqualCaseInsensitive(token.address, fromToken?.address), - ); - const previousTokensWithBalancesFromToken = usePrevious( - tokensWithBalancesFromToken, - ); - - useEffect(() => { - const notDefault = !isSwapsDefaultTokenAddress( - tokensWithBalancesFromToken?.address, - chainId, - ); - const addressesAreTheSame = isEqualCaseInsensitive( - tokensWithBalancesFromToken?.address, - previousTokensWithBalancesFromToken?.address, - ); - const balanceHasChanged = - tokensWithBalancesFromToken?.balance !== - previousTokensWithBalancesFromToken?.balance; - if (notDefault && addressesAreTheSame && balanceHasChanged) { - dispatch( - setSwapsFromToken({ - ...fromToken, - balance: tokensWithBalancesFromToken?.balance, - string: tokensWithBalancesFromToken?.string, - }), - ); - } - }, [ - dispatch, - tokensWithBalancesFromToken, - previousTokensWithBalancesFromToken, - fromToken, - chainId, - ]); - - // If the eth balance changes while on build quote, we update the selected from token - useEffect(() => { - if ( - isSwapsDefaultTokenAddress(fromToken?.address, chainId) && - fromToken?.balance !== hexToDecimal(ethBalance) - ) { - dispatch( - setSwapsFromToken({ - ...fromToken, - balance: hexToDecimal(ethBalance), - string: getValueFromWeiHex({ - value: ethBalance, - numberOfDecimals: 4, - toDenomination: 'ETH', - }), - }), - ); - } - }, [dispatch, fromToken, ethBalance, chainId]); - - useEffect(() => { - if (prevFromTokenBalance !== fromTokenBalance) { - onInputChange(fromTokenInputValue, fromTokenBalance); - } - }, [ - onInputChange, - prevFromTokenBalance, - fromTokenInputValue, - fromTokenBalance, - ]); - - const trackBuildQuotePageLoadedEvent = useCallback(() => { - trackEvent({ - event: 'Build Quote Page Loaded', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }, - }); - }, [ - trackEvent, - hardwareWalletUsed, - hardwareWalletType, - smartTransactionsEnabled, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - ]); - - useEffect(() => { - dispatch(resetSwapsPostFetchState()); - dispatch(setReviewSwapClickedTimestamp()); - trackBuildQuotePageLoadedEvent(); - }, [dispatch, trackBuildQuotePageLoadedEvent]); - - useEffect(() => { - if (smartTransactionsEnabled && smartTransactionFees?.tradeTxFees) { - // We want to clear STX fees, because we only want to use fresh ones on the View Quote page. - clearSmartTransactionFees(); - } - }, [smartTransactionsEnabled, smartTransactionFees]); - - const BlockExplorerLink = () => { - return ( - <a - className="build-quote__token-etherscan-link build-quote__underline" - key="build-quote-etherscan-link" - onClick={() => { - /* istanbul ignore next */ - trackEvent({ - event: MetaMetricsEventName.ExternalLinkClicked, - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: MetaMetricsEventLinkType.TokenTracker, - location: 'Swaps Confirmation', - url_domain: getURLHostName(blockExplorerTokenLink), - }, - }); - global.platform.openTab({ - url: blockExplorerTokenLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerLabel} - </a> - ); - }; - - let tokenVerificationDescription = ''; - if (blockExplorerTokenLink) { - if (occurrences === 1) { - tokenVerificationDescription = t('verifyThisTokenOn', [ - <BlockExplorerLink key="block-explorer-link" />, - ]); - } else if (occurrences === 0) { - tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [ - <BlockExplorerLink key="block-explorer-link" />, - ]); - } - } - - const swapYourTokenBalance = t('swapYourTokenBalance', [ - fromTokenString || '0', - fromTokenSymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol || '', - ]); - - const isDirectWrappingEnabled = shouldEnableDirectWrapping( - chainId, - fromTokenAddress, - selectedToToken.address, - ); - const isReviewSwapButtonDisabled = - fromTokenError || - !isFeatureFlagLoaded || - !Number(fromTokenInputValue) || - !selectedToToken?.address || - !fromTokenAddress || - Number(maxSlippage) < 0 || - Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE || - (toTokenIsNotDefault && occurrences < 2 && !verificationClicked); - - // It's triggered every time there is a change in form values (token from, token to, amount and slippage). - useEffect(() => { - dispatch(clearSwapsQuotes()); - dispatch(stopPollingForQuotes()); - const prefetchQuotesWithoutRedirecting = async () => { - const pageRedirectionDisabled = true; - await dispatch( - fetchQuotesAndSetQuoteState( - history, - fromTokenInputValue, - maxSlippage, - trackEvent, - pageRedirectionDisabled, - ), - ); - }; - // Delay fetching quotes until a user is done typing an input value. If they type a new char in less than a second, - // we will cancel previous setTimeout call and start running a new one. - timeoutIdForQuotesPrefetching = setTimeout(() => { - timeoutIdForQuotesPrefetching = null; - if (!isReviewSwapButtonDisabled) { - // Only do quotes prefetching if the Review swap button is enabled. - prefetchQuotesWithoutRedirecting(); - } - }, 1000); - return () => clearTimeout(timeoutIdForQuotesPrefetching); - }, [ - dispatch, - history, - maxSlippage, - trackEvent, - isReviewSwapButtonDisabled, - fromTokenInputValue, - fromTokenAddress, - toTokenAddress, - smartTransactionsOptInStatus, - ]); - - return ( - <div className="build-quote"> - <div className="build-quote__content"> - <div className="build-quote__dropdown-input-pair-header"> - <div className="build-quote__input-label">{t('swapSwapFrom')}</div> - {!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && ( - <div - className="build-quote__max-button" - data-testid="build-quote__max-button" - onClick={() => - onInputChange(fromTokenBalance || '0', fromTokenBalance) - } - > - {t('max')} - </div> - )} - </div> - <DropdownInputPair - onSelect={onFromSelect} - itemsToSearch={tokensToSearchSwapFrom} - onInputChange={(value) => { - /* istanbul ignore next */ - onInputChange(value, fromTokenBalance); - }} - inputValue={fromTokenInputValue} - leftValue={fromTokenInputValue && swapFromFiatValue} - selectedItem={selectedFromToken} - maxListItems={30} - loading={ - loading && - (!tokensToSearchSwapFrom?.length || - !topAssets || - !Object.keys(topAssets).length) - } - selectPlaceHolderText={t('swapSelect')} - hideItemIf={(item) => - isEqualCaseInsensitive(item.address, selectedToToken?.address) - } - listContainerClassName="build-quote__open-dropdown" - autoFocus - /> - <div - className={classnames('build-quote__balance-message', { - 'build-quote__balance-message--error': - balanceError || fromTokenError, - })} - > - {!fromTokenError && - !balanceError && - fromTokenSymbol && - swapYourTokenBalance} - {!fromTokenError && balanceError && fromTokenSymbol && ( - <div className="build-quite__insufficient-funds"> - <div className="build-quite__insufficient-funds-first"> - {t('swapsNotEnoughForTx', [fromTokenSymbol])} - </div> - <div className="build-quite__insufficient-funds-second"> - {swapYourTokenBalance} - </div> - </div> - )} - {fromTokenError && ( - <> - <div className="build-quote__form-error"> - {t('swapTooManyDecimalsError', [ - fromTokenSymbol, - fromTokenDecimals, - ])} - </div> - <div>{swapYourTokenBalance}</div> - </> - )} - </div> - <div className="build-quote__swap-arrows-row"> - <button - className="build-quote__swap-arrows" - data-testid="build-quote__swap-arrows" - onClick={() => { - onToSelect(selectedFromToken); - onFromSelect(selectedToToken); - }} - > - <i className="fa fa-arrow-up" title={t('swapSwapSwitch')} /> - <i className="fa fa-arrow-down" title={t('swapSwapSwitch')} /> - </button> - </div> - <div className="build-quote__dropdown-swap-to-header"> - <div className="build-quote__input-label">{t('swapSwapTo')}</div> - </div> - <div className="dropdown-input-pair dropdown-input-pair__to"> - <DropdownSearchList - startingItem={selectedToToken} - itemsToSearch={tokensToSearchSwapTo} - fuseSearchKeys={fuseSearchKeys} - selectPlaceHolderText={t('swapSelectAToken')} - maxListItems={30} - onSelect={onToSelect} - loading={ - loading && - (!tokensToSearchSwapTo?.length || - !topAssets || - !Object.keys(topAssets).length) - } - externallySelectedItem={selectedToToken} - hideItemIf={hideDropdownItemIf} - listContainerClassName="build-quote__open-to-dropdown" - hideRightLabels - defaultToAll - shouldSearchForImports - /> - </div> - {toTokenIsNotDefault && - (occurrences < 2 ? ( - <ActionableMessage - type={occurrences === 1 ? 'warning' : 'danger'} - message={ - <div className="build-quote__token-verification-warning-message"> - <div className="build-quote__bold"> - {occurrences === 1 - ? t('swapTokenVerificationOnlyOneSource') - : t('swapTokenVerificationAddedManually')} - </div> - <div>{tokenVerificationDescription}</div> - </div> - } - primaryAction={ - /* istanbul ignore next */ - verificationClicked - ? null - : { - label: t('continue'), - onClick: () => setVerificationClicked(true), - } - } - withRightButton - infoTooltipText={ - blockExplorerTokenLink && - t('swapVerifyTokenExplanation', [blockExplorerLabel]) - } - /> - ) : ( - <div className="build-quote__token-message"> - <span - className="build-quote__bold" - key="token-verification-bold-text" - > - {t('swapTokenVerificationSources', [occurrences])} - </span> - {blockExplorerTokenLink && ( - <> - {t('swapTokenVerificationMessage', [ - <a - className="build-quote__token-etherscan-link" - key="build-quote-etherscan-link" - onClick={() => { - /* istanbul ignore next */ - trackEvent({ - event: 'Clicked Block Explorer Link', - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: 'Token Tracker', - action: 'Swaps Confirmation', - block_explorer_domain: getURLHostName( - blockExplorerTokenLink, - ), - }, - }); - global.platform.openTab({ - url: blockExplorerTokenLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerLabel} - </a>, - ])} - <InfoTooltip - position="top" - contentText={t('swapVerifyTokenExplanation', [ - blockExplorerLabel, - ])} - containerClassName="build-quote__token-tooltip-container" - key="token-verification-info-tooltip" - /> - </> - )} - </div> - ))} - {!isDirectWrappingEnabled && ( - <div className="build-quote__slippage-buttons-container"> - <SlippageButtons - onSelect={(newSlippage) => { - dispatch(setMaxSlippage(newSlippage)); - }} - maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE} - currentSlippage={maxSlippage} - isDirectWrappingEnabled={isDirectWrappingEnabled} - /> - </div> - )} - </div> - <SwapsFooter - onSubmit={ - /* istanbul ignore next */ - async () => { - // We need this to know how long it took to go from clicking on the Review swap button to rendered View Quote page. - dispatch(setReviewSwapClickedTimestamp(Date.now())); - // In case that quotes prefetching is waiting to be executed, but hasn't started yet, - // we want to cancel it and fetch quotes from here. - if (timeoutIdForQuotesPrefetching) { - clearTimeout(timeoutIdForQuotesPrefetching); - dispatch( - fetchQuotesAndSetQuoteState( - history, - fromTokenInputValue, - maxSlippage, - trackEvent, - ), - ); - } else if (areQuotesPresent) { - // If there are prefetched quotes already, go directly to the View Quote page. - history.push(VIEW_QUOTE_ROUTE); - } else { - // If the "Review swap" button was clicked while quotes are being fetched, go to the Loading Quotes page. - await dispatch(setBackgroundSwapRouteState('loading')); - history.push(LOADING_QUOTES_ROUTE); - } - } - } - submitText={t('swapReviewSwap')} - disabled={isReviewSwapButtonDisabled} - hideCancel - showTermsOfService - /> - </div> - ); -} - -BuildQuote.propTypes = { - ethBalance: PropTypes.string, - selectedAccountAddress: PropTypes.string, - shuffledTokensList: PropTypes.array, -}; diff --git a/ui/pages/swaps/build-quote/build-quote.stories.js b/ui/pages/swaps/build-quote/build-quote.stories.js deleted file mode 100644 index 008b4b4a3ed4..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.stories.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { shuffle } from 'lodash'; -import testData from '../../../../.storybook/test-data'; -import BuildQuote from './build-quote'; - -const tokenValuesArr = shuffle(testData.metamask.tokenList); - -export default { - title: 'Pages/Swaps/BuildQuote', - - argTypes: { - ethBalance: { - control: { type: 'text' }, - }, - selectedAccountAddress: { - control: { type: 'text' }, - }, - shuffledTokensList: { control: 'object' }, - }, - args: { - ethBalance: '0x8', - selectedAccountAddress: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - shuffledTokensList: tokenValuesArr, - }, -}; - -export const DefaultStory = (args) => { - return ( - <> - <BuildQuote {...args} /> - </> - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/build-quote/build-quote.test.js b/ui/pages/swaps/build-quote/build-quote.test.js deleted file mode 100644 index aa8738e43ecb..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.test.js +++ /dev/null @@ -1,223 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import { createTestProviderTools } from '../../../../test/stub/provider'; -import { - setSwapsFromToken, - setSwapToToken, - setFromTokenInputValue, -} from '../../../ducks/swaps/swaps'; -import { mockNetworkState } from '../../../../test/stub/networks'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import BuildQuote from '.'; - -const middleware = [thunk]; -const createProps = (customProps = {}) => { - return { - ethBalance: '0x8', - selectedAccountAddress: 'selectedAccountAddress', - isFeatureFlagLoaded: false, - shuffledTokensList: [], - ...customProps, - }; -}; - -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - ignoreTokens: jest.fn(), - setBackgroundSwapRouteState: jest.fn(), - clearSwapsQuotes: jest.fn(), - stopPollingForQuotes: jest.fn(), - clearSmartTransactionFees: jest.fn(), - setSwapsFromToken: jest.fn(), - setSwapToToken: jest.fn(), - setFromTokenInputValue: jest.fn(), -}); - -jest.mock('../../../ducks/swaps/swaps', () => { - const actual = jest.requireActual('../../../ducks/swaps/swaps'); - return { - ...actual, - setSwapsFromToken: jest.fn(), - setSwapToToken: jest.fn(), - setFromTokenInputValue: jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }), - }; -}); - -jest.mock('../swaps.util', () => { - const actual = jest.requireActual('../swaps.util'); - return { - ...actual, - fetchTokenBalance: jest.fn(() => Promise.resolve()), - fetchTokenPrice: jest.fn(() => Promise.resolve()), - }; -}); - -const providerResultStub = { - eth_getCode: '0x123', - eth_call: - '0x00000000000000000000000000000000000000000000000029a2241af62c0000', -}; -const { provider } = createTestProviderTools({ - scaffold: providerResultStub, - networkId: '5', - chainId: '5', -}); - -describe('BuildQuote', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - - beforeEach(() => { - global.ethereumProvider = provider; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect(getByText('Swap to')).toBeInTheDocument(); - expect(getByText('Select')).toBeInTheDocument(); - expect(getByText('Slippage tolerance')).toBeInTheDocument(); - expect(getByText('2%')).toBeInTheDocument(); - expect(getByText('3%')).toBeInTheDocument(); - expect(getByText('Review swap')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); - }); - - it('switches swap from and to tokens', () => { - const setSwapFromTokenMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setSwapsFromToken.mockImplementation(setSwapFromTokenMock); - const setSwapToTokenMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setSwapToToken.mockImplementation(setSwapToTokenMock); - const mockStore = createSwapsMockStore(); - const store = configureMockStore(middleware)(mockStore); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - <BuildQuote {...props} />, - store, - ); - expect(getByText('Swap from')).toBeInTheDocument(); - fireEvent.click(getByTestId('build-quote__swap-arrows')); - expect(setSwapsFromToken).toHaveBeenCalledWith(mockStore.swaps.toToken); - expect(setSwapToToken).toHaveBeenCalled(); - }); - - it('renders the block explorer link, only 1 verified source', () => { - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 1; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect(getByText('Only verified on 1 source.')).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); - }); - - it('renders the block explorer link, 0 verified sources', () => { - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 0; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect( - getByText('This token has been added manually.'), - ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); - }); - - it('clicks on a block explorer link', () => { - global.platform = { openTab: jest.fn() }; - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 1; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - const blockExplorer = getByText('etherscan.io'); - expect(blockExplorer).toBeInTheDocument(); - fireEvent.click(blockExplorer); - expect(global.platform.openTab).toHaveBeenCalledWith({ - url: 'https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - }); - }); - - it('clicks on the "max" link', () => { - const setFromTokenInputValueMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setFromTokenInputValue.mockImplementation(setFromTokenInputValueMock); - const mockStore = createSwapsMockStore(); - mockStore.swaps.fromToken = 'DAI'; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(<BuildQuote {...props} />, store); - const maxLink = getByText('Max'); - fireEvent.click(maxLink); - expect(setFromTokenInputValue).toHaveBeenCalled(); - }); -}); diff --git a/ui/pages/swaps/build-quote/index.js b/ui/pages/swaps/build-quote/index.js deleted file mode 100644 index 772229f2a187..000000000000 --- a/ui/pages/swaps/build-quote/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './build-quote'; diff --git a/ui/pages/swaps/build-quote/index.scss b/ui/pages/swaps/build-quote/index.scss deleted file mode 100644 index 5d454002a0b8..000000000000 --- a/ui/pages/swaps/build-quote/index.scss +++ /dev/null @@ -1,223 +0,0 @@ -@use "design-system"; - -.build-quote { - display: flex; - flex-flow: column; - align-items: center; - flex: 1; - width: 100%; - padding-top: 4px; - - &__content { - display: flex; - height: 100%; - flex-direction: column; - padding-left: 24px; - padding-right: 24px; - } - - &__content { - display: flex; - } - - &__dropdown-swap-to-header { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-top: 0; - margin-bottom: 12px; - } - - &__dropdown-input-pair-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - width: 100%; - margin-bottom: 12px; - flex: 0.5 1 auto; - max-height: 56px; - } - - &__title, - &__input-label { - @include design-system.H5; - - font-weight: bold; - color: var(--color-text-default); - margin-top: 3px; - } - - &__swap-arrows-row { - width: 100%; - display: flex; - justify-content: flex-end; - padding-right: 16px; - padding-top: 12px; - height: 24px; - position: relative; - } - - &__swap-arrows { - display: flex; - flex: 0 0 auto; - height: 24px; - cursor: pointer; - background: unset; - color: var(--color-icon-muted); - } - - &__max-button { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - } - - &__balance-message { - @include design-system.H7; - - width: 100%; - color: var(--color-text-muted); - margin-top: 4px; - display: flex; - flex-flow: column; - height: 18px; - - &--error { - div:first-of-type { - font-weight: bold; - color: var(--color-text-default); - } - - .build-quote__form-error:first-of-type { - font-weight: bold; - color: var(--color-error-default); - } - - div:last-of-type { - font-weight: normal; - color: var(--color-text-alternative); - } - } - } - - &__slippage-buttons-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 32px; - } - - &__open-dropdown, - &__open-to-dropdown { - max-height: 330px; - box-shadow: var(--shadow-size-sm) var(--color-shadow-default); - position: absolute; - width: 100%; - } - - .dropdown-input-pair { - .searchable-item-list { - &__item--add-token { - display: none; - } - } - - &__to { - .searchable-item-list { - &__item--add-token { - display: flex; - } - } - } - - &__input { - div { - border: 1px solid var(--color-border-default); - border-left: 0; - } - } - } - - &__open-to-dropdown { - max-height: 194px; - - @include design-system.screen-sm-min { - max-height: 276px; - } - } - - &__token-message { - @include design-system.H7; - - width: 100%; - color: var(--color-text-alternative); - margin-top: 4px; - - .info-tooltip { - display: inline-block; - } - } - - &__token-etherscan-link { - color: var(--color-primary-default); - cursor: pointer; - } - - &__token-tooltip-container { - // Needed to override the style property added by the react-tippy library - display: flex !important; - } - - &__bold { - font-weight: bold; - } - - &__underline { - text-decoration: underline; - } - - /* Prevents the swaps "Swap to" field from overflowing */ - .dropdown-input-pair__to .dropdown-search-list { - width: 100%; - } -} - -@keyframes slide-in { - 100% { transform: translateY(0%); } -} - -.smart-transactions-popover { - transform: translateY(-100%); - animation: slide-in 0.5s forwards; - - &__content { - flex-direction: column; - - ul { - list-style: inside; - } - - a { - color: var(--color-primary-default); - cursor: pointer; - } - } - - &__footer { - flex-direction: column; - flex: 1; - align-items: center; - border-top: 0; - - button { - border-radius: 50px; - } - - a { - font-size: inherit; - padding-bottom: 0; - } - } -} diff --git a/ui/pages/swaps/create-new-swap/create-new-swap.js b/ui/pages/swaps/create-new-swap/create-new-swap.js index 3f19b68631b2..6d7963e36ca8 100644 --- a/ui/pages/swaps/create-new-swap/create-new-swap.js +++ b/ui/pages/swaps/create-new-swap/create-new-swap.js @@ -9,7 +9,7 @@ import { I18nContext } from '../../../contexts/i18n'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { - navigateBackToBuildQuote, + navigateBackToPrepareSwap, setSwapsFromToken, } from '../../../ducks/swaps/swaps'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; @@ -32,7 +32,7 @@ export default function CreateNewSwap({ sensitiveTrackingProperties }) { sensitiveProperties: sensitiveTrackingProperties, }); history.push(DEFAULT_ROUTE); // It cleans up Swaps state. - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); dispatch(setSwapsFromToken(defaultSwapsToken)); }} > diff --git a/ui/pages/swaps/create-new-swap/create-new-swap.test.js b/ui/pages/swaps/create-new-swap/create-new-swap.test.js index 0ce6fa400150..86accf04da23 100644 --- a/ui/pages/swaps/create-new-swap/create-new-swap.test.js +++ b/ui/pages/swaps/create-new-swap/create-new-swap.test.js @@ -10,7 +10,7 @@ import { } from '../../../../test/jest'; import { setSwapsFromToken, - navigateBackToBuildQuote, + navigateBackToPrepareSwap, } from '../../../ducks/swaps/swaps'; import CreateNewSwap from '.'; @@ -23,7 +23,7 @@ const createProps = (customProps = {}) => { }; const backgroundConnection = { - navigateBackToBuildQuote: jest.fn(), + navigateBackToPrepareSwap: jest.fn(), setBackgroundSwapRouteState: jest.fn(), navigatedBackToBuildQuote: jest.fn(), }; @@ -35,7 +35,7 @@ jest.mock('../../../ducks/swaps/swaps', () => { return { ...actual, setSwapsFromToken: jest.fn(), - navigateBackToBuildQuote: jest.fn(), + navigateBackToPrepareSwap: jest.fn(), }; }); @@ -63,12 +63,12 @@ describe('CreateNewSwap', () => { }; }); setSwapsFromToken.mockImplementation(setSwapFromTokenMock); - const navigateBackToBuildQuoteMock = jest.fn(() => { + const navigateBackToPrepareSwapMock = jest.fn(() => { return { type: 'MOCK_ACTION', }; }); - navigateBackToBuildQuote.mockImplementation(navigateBackToBuildQuoteMock); + navigateBackToPrepareSwap.mockImplementation(navigateBackToPrepareSwapMock); const store = configureMockStore(middleware)(createSwapsMockStore()); const { getByText } = renderWithProvider( @@ -77,6 +77,6 @@ describe('CreateNewSwap', () => { ); await fireEvent.click(getByText('Create a new swap')); expect(setSwapFromTokenMock).toHaveBeenCalledTimes(1); - expect(navigateBackToBuildQuoteMock).toHaveBeenCalledTimes(1); + expect(navigateBackToPrepareSwapMock).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/swaps/dropdown-input-pair/README.mdx b/ui/pages/swaps/dropdown-input-pair/README.mdx deleted file mode 100644 index cac84e714daa..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/README.mdx +++ /dev/null @@ -1,15 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; - -import DropdownInputPair from '.'; - -# Dropdown Input Pair - -Dropdown to choose cryptocurrency with amount input field. - -<Canvas> - <Story id="pages-swaps-dropdowninputpair--default-story" /> -</Canvas> - -## Props - -<ArgsTable of={DropdownInputPair} /> diff --git a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap deleted file mode 100644 index 01a3a89ad4c5..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DropdownInputPair renders the component with initial props 1`] = ` -<div - 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" - > - <input - aria-invalid="false" - class="MuiInputBase-input MuiInput-input MuiInputBase-inputMarginDense MuiInput-inputMarginDense" - dir="auto" - placeholder="0" - type="text" - value="" - /> - </div> -</div> -`; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js deleted file mode 100644 index 9da47ddf80ea..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js +++ /dev/null @@ -1,177 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import DropdownSearchList from '../dropdown-search-list'; -import TextField from '../../../components/ui/text-field'; - -const characterWidthMap = { - 1: 5.86, - 2: 10.05, - 3: 10.45, - 4: 11.1, - 5: 10, - 6: 10.06, - 7: 9.17, - 8: 10.28, - 9: 10.06, - 0: 11.22, - '.': 4.55, -}; - -const getInputWidth = (value) => { - const valueString = String(value); - const charArray = valueString.split(''); - return charArray.reduce( - (inputWidth, _char) => inputWidth + characterWidthMap[_char], - 12, - ); -}; -export default function DropdownInputPair({ - itemsToSearch = [], - onInputChange, - inputValue = '', - onSelect, - leftValue, - selectedItem, - SearchListPlaceholder, - maxListItems, - selectPlaceHolderText, - loading, - hideItemIf, - listContainerClassName, - autoFocus, -}) { - const [isOpen, setIsOpen] = useState(false); - const open = () => setIsOpen(true); - const close = () => setIsOpen(false); - const inputRef = useRef(); - const onTextFieldChange = (event) => { - event.stopPropagation(); - // Automatically prefix value with 0. if user begins typing . - const valueToUse = event.target.value === '.' ? '0.' : event.target.value; - - // Regex that validates strings with only numbers, 'x.', '.x', and 'x.x' - const regexp = /^(\.\d+|\d+(\.\d+)?|\d+\.)$/u; - // If the value is either empty or contains only numbers and '.' and only has one '.', update input to match - if (valueToUse === '' || regexp.test(valueToUse)) { - onInputChange(valueToUse); - } else { - // otherwise, use the previously set inputValue (effectively denying the user from inputting the last char) - // or an empty string if we do not yet have an inputValue - onInputChange(inputValue || ''); - } - }; - const [applyTwoLineStyle, setApplyTwoLineStyle] = useState(null); - useEffect(() => { - setApplyTwoLineStyle( - (inputRef?.current?.getBoundingClientRect()?.width || 0) + - getInputWidth(inputValue || '') > - 137, - ); - }, [inputValue, inputRef]); - - return ( - <div className="dropdown-input-pair"> - <DropdownSearchList - itemsToSearch={itemsToSearch} - SearchListPlaceholder={SearchListPlaceholder} - fuseSearchKeys={[ - { name: 'name', weight: 0.499 }, - { name: 'symbol', weight: 0.499 }, - { name: 'address', weight: 0.002 }, - ]} - maxListItems={maxListItems} - onOpen={open} - onClose={close} - onSelect={onSelect} - className={isOpen ? 'dropdown-input-pair__list--full-width' : ''} - externallySelectedItem={selectedItem} - selectPlaceHolderText={selectPlaceHolderText} - selectorClosedClassName="dropdown-input-pair__selector--closed" - listContainerClassName={listContainerClassName} - loading={loading} - hideItemIf={hideItemIf} - defaultToAll - /> - {!isOpen && ( - <TextField - className={classnames('dropdown-input-pair__input', { - 'dropdown-input-pair__two-line-input': applyTwoLineStyle, - })} - type="text" - placeholder="0" - onChange={onTextFieldChange} - fullWidth - margin="dense" - value={inputValue} - autoFocus={autoFocus} - /> - )} - {!isOpen && leftValue && ( - <div - className={classnames('dropdown-input-pair__left-value', { - 'dropdown-input-pair__left-value--two-lines': applyTwoLineStyle, - })} - ref={inputRef} - > - ≈ {leftValue} - </div> - )} - </div> - ); -} - -DropdownInputPair.propTypes = { - /** - * Give items data for the component - */ - itemsToSearch: PropTypes.array, - /** - * Handler for input change - */ - onInputChange: PropTypes.func, - /** - * Show input value content - */ - inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * Handler for onSelect - */ - onSelect: PropTypes.func, - /** - * Set value to left - */ - leftValue: PropTypes.string, - /** - * Show selected item - */ - selectedItem: PropTypes.object, - /** - * Doesn't look like this is used - */ - SearchListPlaceholder: PropTypes.func, - /** - * Define maximum item per list - */ - maxListItems: PropTypes.number, - /** - * Show select placeholder text - */ - selectPlaceHolderText: PropTypes.string, - /** - * Check if the component is loading - */ - loading: PropTypes.bool, - /** - * Handler for hide item - */ - hideItemIf: PropTypes.func, - /** - * Add custom CSS class for list container - */ - listContainerClassName: PropTypes.string, - /** - * Check if the component is auto focus - */ - autoFocus: PropTypes.bool, -}; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js deleted file mode 100644 index ff5a6c756c1b..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react'; -import { useArgs } from '@storybook/client-api'; - -import README from './README.mdx'; -import DropdownInputPair from '.'; - -const tokens = [ - { - primaryLabel: 'MetaMark (META)', - name: 'MetaMark', - iconUrl: '.storybook/images/metamark.svg', - erc20: true, - decimals: 18, - symbol: 'META', - address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', - }, - { - primaryLabel: '0x (ZRX)', - name: '0x', - iconUrl: '.storybook/images/0x.svg', - erc20: true, - symbol: 'ZRX', - decimals: 18, - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - }, - { - primaryLabel: 'AirSwap Token (AST)', - name: 'AirSwap Token', - iconUrl: '.storybook/images/AST.png', - erc20: true, - symbol: 'AST', - decimals: 4, - address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', - }, - { - primaryLabel: 'Basic Attention Token (BAT)', - name: 'Basic Attention Token', - iconUrl: '.storybook/images/BAT_icon.svg', - erc20: true, - symbol: 'BAT', - decimals: 18, - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - }, - { - primaryLabel: 'Civil Token (CVL)', - name: 'Civil Token', - iconUrl: '.storybook/images/CVL_token.svg', - erc20: true, - symbol: 'CVL', - decimals: 18, - address: '0x01FA555c97D7958Fa6f771f3BbD5CCD508f81e22', - }, - { - primaryLabel: 'Gladius (GLA)', - name: 'Gladius', - iconUrl: '.storybook/images/gladius.svg', - erc20: true, - symbol: 'GLA', - decimals: 8, - address: '0x71D01dB8d6a2fBEa7f8d434599C237980C234e4C', - }, - { - primaryLabel: 'Gnosis Token (GNO)', - name: 'Gnosis Token', - iconUrl: '.storybook/images/gnosis.svg', - erc20: true, - symbol: 'GNO', - decimals: 18, - address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', - }, - { - primaryLabel: 'OmiseGO (OMG)', - name: 'OmiseGO', - iconUrl: '.storybook/images/omg.jpg', - erc20: true, - symbol: 'OMG', - decimals: 18, - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - }, - { - primaryLabel: 'Sai Stablecoin v1.0 (SAI)', - name: 'Sai Stablecoin v1.0', - iconUrl: '.storybook/images/sai.svg', - erc20: true, - symbol: 'SAI', - decimals: 18, - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - }, - { - primaryLabel: 'Tether USD (USDT)', - name: 'Tether USD', - iconUrl: '.storybook/images/tether_usd.png', - erc20: true, - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - }, - { - primaryLabel: 'WednesdayCoin (WED)', - name: 'WednesdayCoin', - iconUrl: '.storybook/images/wed.png', - erc20: true, - symbol: 'WED', - decimals: 18, - address: '0x7848ae8F19671Dc05966dafBeFbBbb0308BDfAbD', - }, - { - primaryLabel: 'Wrapped BTC (WBTC)', - name: 'Wrapped BTC', - iconUrl: '.storybook/images/wbtc.png', - erc20: true, - symbol: 'WBTC', - decimals: 8, - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - }, -]; - -export default { - title: 'Pages/Swaps/DropdownInputPair', - - component: DropdownInputPair, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - itemsToSearch: { control: 'array' }, - onInputChange: { action: 'onInputChange' }, - inputValue: { control: 'text' }, - onSelect: { action: 'onSelect' }, - leftValue: { control: 'text' }, - selectedItem: { control: 'object' }, - maxListItems: { control: 'number' }, - selectPlaceHolderText: { control: 'text' }, - loading: { control: 'boolean' }, - listContainerClassName: { control: 'text' }, - autoFocus: { control: 'boolean' }, - }, -}; - -const tokensToSearch = tokens.map((token) => ({ - ...token, - primaryLabel: token.symbol, - secondaryLabel: token.name, - rightPrimaryLabel: `${(Math.random() * 100).toFixed( - Math.floor(Math.random() * 6), - )} ${token.symbol}`, - rightSecondaryLabel: `$${(Math.random() * 1000).toFixed(2)}`, -})); - -export const DefaultStory = (args) => { - const [{ inputValue, selectedItem = tokensToSearch[0] }, updateArgs] = - useArgs(); - return ( - <DropdownInputPair - {...args} - inputValue={inputValue} - onInputChange={(value) => { - updateArgs({ ...args, inputValue: value }); - }} - selectedItem={selectedItem} - /> - ); -}; - -DefaultStory.storyName = 'Default'; - -DefaultStory.args = { - itemsToSearch: tokensToSearch, - maxListItems: tokensToSearch.length, - loading: false, -}; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js deleted file mode 100644 index e9f319d25fcc..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import DropdownInputPair from '.'; - -const createProps = (customProps = {}) => { - return { - onInputChange: jest.fn(), - ...customProps, - }; -}; - -describe('DropdownInputPair', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByPlaceholderText } = renderWithProvider( - <DropdownInputPair {...props} />, - store, - ); - expect(getByPlaceholderText('0')).toBeInTheDocument(); - expect( - document.querySelector('.dropdown-input-pair__input'), - ).toMatchSnapshot(); - }); - - it('changes the input field', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByPlaceholderText } = renderWithProvider( - <DropdownInputPair {...props} />, - store, - ); - fireEvent.change(getByPlaceholderText('0'), { - target: { value: 1.1 }, - }); - expect(props.onInputChange).toHaveBeenCalledWith('1.1'); - }); -}); diff --git a/ui/pages/swaps/dropdown-input-pair/index.js b/ui/pages/swaps/dropdown-input-pair/index.js deleted file mode 100644 index d89fc83b8de2..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './dropdown-input-pair'; diff --git a/ui/pages/swaps/dropdown-input-pair/index.scss b/ui/pages/swaps/dropdown-input-pair/index.scss deleted file mode 100644 index 30d5440e0de6..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/index.scss +++ /dev/null @@ -1,78 +0,0 @@ -@use "design-system"; - -.dropdown-input-pair { - display: flex; - width: 312px; - height: 60px; - position: relative; - - &__input { - margin: 0 !important; - - input { - @include design-system.H4; - - padding-top: 6px; - } - - div { - border: 1px solid var(--color-border-default); - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; - height: 60px; - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ - } - - input[type=number] { - -moz-appearance: textfield; /* Firefox */ - } - } - - &__list { - &--full-width { - width: 100%; - } - } - - &__left-value { - @include design-system.H7; - - position: absolute; - right: 16px; - height: 100%; - display: flex; - align-items: center; - color: var(--color-text-alternative); - - &--two-lines { - right: inherit; - left: 157px; - align-items: unset; - top: 34px; - } - } - - .dropdown-input-pair__selector--closed { - height: 60px; - width: 142px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &__two-line-input { - div { - align-items: flex-start; - } - - input { - padding-top: 14px; - } - } -} diff --git a/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap b/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap deleted file mode 100644 index 6057b37ee370..000000000000 --- a/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DropdownSearchList renders the component with initial props 1`] = ` -<div> - <div - class="dropdown-search-list" - data-testid="dropdown-search-list" - tabindex="0" - > - <div - class="dropdown-search-list__selector-closed-container" - > - <div - class="dropdown-search-list__selector-closed" - > - <div - class="" - > - <img - alt="symbol" - class="url-icon dropdown-search-list__selector-closed-icon" - src="iconUrl" - /> - </div> - <div - class="dropdown-search-list__labels" - > - <div - class="dropdown-search-list__item-labels" - > - <span - class="dropdown-search-list__closed-primary-label" - > - symbol - </span> - </div> - </div> - </div> - <span - class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/arrow-down.svg');" - /> - </div> - </div> -</div> -`; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js deleted file mode 100644 index 1182ad12d72a..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js +++ /dev/null @@ -1,334 +0,0 @@ -import React, { - useState, - useCallback, - useEffect, - useContext, - useRef, -} from 'react'; -import { useSelector } from 'react-redux'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { isEqual } from 'lodash'; -import { I18nContext } from '../../../contexts/i18n'; -import SearchableItemList from '../searchable-item-list'; -import PulseLoader from '../../../components/ui/pulse-loader'; -import UrlIcon from '../../../components/ui/url-icon'; -import { - Icon, - IconName, - IconSize, -} from '../../../components/component-library'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import ImportToken from '../import-token'; -import { - isHardwareWallet, - getHardwareWalletType, - getCurrentChainId, - getRpcPrefsForCurrentProvider, -} from '../../../selectors/selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; -import { getURLHostName } from '../../../helpers/utils/util'; -import { getCurrentSmartTransactionsEnabled } from '../../../ducks/swaps/swaps'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; - -export default function DropdownSearchList({ - searchListClassName, - itemsToSearch, - selectPlaceHolderText, - fuseSearchKeys, - defaultToAll, - maxListItems, - onSelect, - startingItem, - onOpen, - onClose, - className = '', - externallySelectedItem, - selectorClosedClassName, - loading, - hideRightLabels, - hideItemIf, - listContainerClassName, - shouldSearchForImports, -}) { - const t = useContext(I18nContext); - const [isOpen, setIsOpen] = useState(false); - const [isImportTokenModalOpen, setIsImportTokenModalOpen] = useState(false); - const [selectedItem, setSelectedItem] = useState(startingItem); - const [tokenForImport, setTokenForImport] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - - const trackEvent = useContext(MetaMetricsContext); - - const close = useCallback(() => { - setIsOpen(false); - onClose?.(); - }, [onClose]); - - const onClickItem = useCallback( - (item) => { - onSelect?.(item); - setSelectedItem(item); - close(); - }, - [onSelect, close], - ); - - const onOpenImportTokenModalClick = (item) => { - setTokenForImport(item); - setIsImportTokenModalOpen(true); - }; - - /* istanbul ignore next */ - const onImportTokenClick = () => { - trackEvent({ - event: 'Token Imported', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - symbol: tokenForImport?.symbol, - address: tokenForImport?.address, - chain_id: chainId, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }, - }); - // Only when a user confirms import of a token, we add it and show it in a dropdown. - onSelect?.(tokenForImport); - setSelectedItem(tokenForImport); - setTokenForImport(null); - close(); - }; - - const onImportTokenCloseClick = () => { - setIsImportTokenModalOpen(false); - close(); - }; - - const onClickSelector = useCallback(() => { - if (!isOpen) { - setIsOpen(true); - onOpen?.(); - } - }, [isOpen, onOpen]); - - const prevExternallySelectedItemRef = useRef(); - useEffect(() => { - prevExternallySelectedItemRef.current = externallySelectedItem; - }); - const prevExternallySelectedItem = prevExternallySelectedItemRef.current; - - useEffect(() => { - if ( - externallySelectedItem && - !isEqual(externallySelectedItem, selectedItem) - ) { - setSelectedItem(externallySelectedItem); - } else if (prevExternallySelectedItem && !externallySelectedItem) { - setSelectedItem(null); - } - }, [externallySelectedItem, selectedItem, prevExternallySelectedItem]); - - const onKeyUp = (e) => { - if (e.key === 'Escape') { - close(); - } else if (e.key === 'Enter') { - onClickSelector(e); - } - }; - - const blockExplorerLink = - rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? - null; - - const blockExplorerHostName = getURLHostName(blockExplorerLink); - - const importTokenProps = { - onImportTokenCloseClick, - onImportTokenClick, - setIsImportTokenModalOpen, - tokenForImport, - }; - - return ( - <div - className={classnames('dropdown-search-list', className)} - data-testid="dropdown-search-list" - onClick={onClickSelector} - onKeyUp={onKeyUp} - tabIndex="0" - > - {tokenForImport && isImportTokenModalOpen && ( - <ImportToken isOpen {...importTokenProps} /> - )} - {!isOpen && ( - <div - className={classnames( - 'dropdown-search-list__selector-closed-container', - selectorClosedClassName, - )} - > - <div className="dropdown-search-list__selector-closed"> - {selectedItem?.iconUrl && ( - <UrlIcon - url={selectedItem.iconUrl} - className="dropdown-search-list__selector-closed-icon" - name={selectedItem?.symbol} - /> - )} - {!selectedItem?.iconUrl && ( - <div className="dropdown-search-list__default-dropdown-icon" /> - )} - <div className="dropdown-search-list__labels"> - <div className="dropdown-search-list__item-labels"> - <span - className={classnames( - 'dropdown-search-list__closed-primary-label', - { - 'dropdown-search-list__select-default': - !selectedItem?.symbol, - }, - )} - > - {selectedItem?.symbol || selectPlaceHolderText} - </span> - </div> - </div> - </div> - <Icon name={IconName.ArrowDown} size={IconSize.Xs} marginRight={3} /> - </div> - )} - {isOpen && ( - <> - <SearchableItemList - itemsToSearch={loading ? [] : itemsToSearch} - Placeholder={() => - /* istanbul ignore next */ - loading ? ( - <div className="dropdown-search-list__loading-item"> - <PulseLoader /> - <div className="dropdown-search-list__loading-item-text-container"> - <span className="dropdown-search-list__loading-item-text"> - {t('swapFetchingTokens')} - </span> - </div> - </div> - ) : ( - <div className="dropdown-search-list__placeholder"> - {t('swapBuildQuotePlaceHolderText', [searchQuery])} - {blockExplorerLink && ( - <div - tabIndex="0" - className="searchable-item-list__item searchable-item-list__item--add-token" - key="searchable-item-list-item-last" - > - <ActionableMessage - message={t('addTokenByContractAddress', [ - <a - key="dropdown-search-list__etherscan-link" - onClick={() => { - trackEvent({ - event: 'Clicked Block Explorer Link', - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: 'Token Tracker', - action: 'Verify Contract Address', - block_explorer_domain: blockExplorerHostName, - }, - }); - global.platform.openTab({ - url: blockExplorerLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerHostName} - </a>, - ])} - /> - </div> - )} - </div> - ) - } - searchPlaceholderText={t('swapSearchNameOrAddress')} - fuseSearchKeys={fuseSearchKeys} - defaultToAll={defaultToAll} - onClickItem={onClickItem} - onOpenImportTokenModalClick={onOpenImportTokenModalClick} - maxListItems={maxListItems} - className={classnames( - 'dropdown-search-list__token-container', - searchListClassName, - { - 'dropdown-search-list--open': isOpen, - }, - )} - hideRightLabels={hideRightLabels} - hideItemIf={hideItemIf} - listContainerClassName={listContainerClassName} - shouldSearchForImports={shouldSearchForImports} - searchQuery={searchQuery} - setSearchQuery={setSearchQuery} - /> - <div - className="dropdown-search-list__close-area" - data-testid="dropdown-search-list__close-area" - onClick={(event) => { - event.stopPropagation(); - setIsOpen(false); - onClose?.(); - }} - /> - </> - )} - </div> - ); -} - -DropdownSearchList.propTypes = { - itemsToSearch: PropTypes.array, - onSelect: PropTypes.func, - searchListClassName: PropTypes.string, - fuseSearchKeys: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - weight: PropTypes.number, - }), - ), - defaultToAll: PropTypes.bool, - maxListItems: PropTypes.number, - startingItem: PropTypes.object, - onOpen: PropTypes.func, - onClose: PropTypes.func, - className: PropTypes.string, - externallySelectedItem: PropTypes.object, - loading: PropTypes.bool, - selectPlaceHolderText: PropTypes.string, - selectorClosedClassName: PropTypes.string, - hideRightLabels: PropTypes.bool, - hideItemIf: PropTypes.func, - listContainerClassName: PropTypes.string, - shouldSearchForImports: PropTypes.bool, -}; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js deleted file mode 100644 index 73ec3ea7aec8..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import DropdownSearchList from '.'; - -const tokens = [ - { - primaryLabel: 'MetaMark (META)', - name: 'MetaMark', - iconUrl: 'metamark.svg', - erc20: true, - decimals: 18, - symbol: 'META', - address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', - }, - { - primaryLabel: '0x (ZRX)', - name: '0x', - iconUrl: '0x.svg', - erc20: true, - symbol: 'ZRX', - decimals: 18, - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - }, - { - primaryLabel: 'AirSwap Token (AST)', - name: 'AirSwap Token', - iconUrl: 'AST.png', - erc20: true, - symbol: 'AST', - decimals: 4, - address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', - }, - { - primaryLabel: 'Basic Attention Token (BAT)', - name: 'Basic Attention Token', - iconUrl: 'BAT_icon.svg', - erc20: true, - symbol: 'BAT', - decimals: 18, - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - }, - { - primaryLabel: 'Civil Token (CVL)', - name: 'Civil Token', - iconUrl: 'CVL_token.svg', - erc20: true, - symbol: 'CVL', - decimals: 18, - address: '0x01FA555c97D7958Fa6f771f3BbD5CCD508f81e22', - }, - { - primaryLabel: 'Gladius (GLA)', - name: 'Gladius', - iconUrl: 'gladius.svg', - erc20: true, - symbol: 'GLA', - decimals: 8, - address: '0x71D01dB8d6a2fBEa7f8d434599C237980C234e4C', - }, - { - primaryLabel: 'Gnosis Token (GNO)', - name: 'Gnosis Token', - iconUrl: 'gnosis.svg', - erc20: true, - symbol: 'GNO', - decimals: 18, - address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', - }, - { - primaryLabel: 'OmiseGO (OMG)', - name: 'OmiseGO', - iconUrl: 'omg.jpg', - erc20: true, - symbol: 'OMG', - decimals: 18, - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - }, - { - primaryLabel: 'Sai Stablecoin v1.0 (SAI)', - name: 'Sai Stablecoin v1.0', - iconUrl: 'sai.svg', - erc20: true, - symbol: 'SAI', - decimals: 18, - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - }, - { - primaryLabel: 'Tether USD (USDT)', - name: 'Tether USD', - iconUrl: 'tether_usd.png', - erc20: true, - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - }, - { - primaryLabel: 'WednesdayCoin (WED)', - name: 'WednesdayCoin', - iconUrl: 'wed.png', - erc20: true, - symbol: 'WED', - decimals: 18, - address: '0x7848ae8F19671Dc05966dafBeFbBbb0308BDfAbD', - }, - { - primaryLabel: 'Wrapped BTC (WBTC)', - name: 'Wrapped BTC', - iconUrl: 'wbtc.png', - erc20: true, - symbol: 'WBTC', - decimals: 8, - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - }, -]; - -export default { - title: 'Pages/Swaps/DropdownSearchList', -}; - -const tokensToSearch = tokens.map((token) => ({ - ...token, - primaryLabel: token.symbol, - secondaryLabel: token.name, - rightPrimaryLabel: `${(Math.random() * 100).toFixed( - Math.floor(Math.random() * 6), - )} ${token.symbol}`, - rightSecondaryLabel: `$${(Math.random() * 1000).toFixed(2)}`, -})); - -export const DefaultStory = () => { - return ( - <div style={{ height: '82vh', width: '357px' }}> - <DropdownSearchList - startingItem={tokensToSearch[0]} - itemsToSearch={tokensToSearch} - searchPlaceholderText="Search for a token" - fuseSearchKeys={[ - { name: 'name', weight: 0.5 }, - { name: 'symbol', weight: 0.5 }, - ]} - maxListItems={tokensToSearch.length} - defaultToAll - /> - </div> - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js deleted file mode 100644 index f0ae4a889169..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import DropdownSearchList from '.'; - -const createProps = (customProps = {}) => { - return { - startingItem: { - iconUrl: 'iconUrl', - symbol: 'symbol', - }, - ...customProps, - }; -}; - -jest.mock('../searchable-item-list', () => jest.fn(() => null)); - -describe('DropdownSearchList', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { container, getByText } = renderWithProvider( - <DropdownSearchList {...props} />, - store, - ); - expect(container).toMatchSnapshot(); - expect(getByText('symbol')).toBeInTheDocument(); - }); - - it('renders the component, opens the list and closes it', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByTestId } = renderWithProvider( - <DropdownSearchList {...props} />, - store, - ); - const dropdownSearchList = getByTestId('dropdown-search-list'); - expect(dropdownSearchList).toBeInTheDocument(); - fireEvent.click(dropdownSearchList); - const closeButton = getByTestId('dropdown-search-list__close-area'); - expect(closeButton).toBeInTheDocument(); - fireEvent.click(closeButton); - expect(closeButton).not.toBeInTheDocument(); - }); -}); diff --git a/ui/pages/swaps/dropdown-search-list/index.js b/ui/pages/swaps/dropdown-search-list/index.js deleted file mode 100644 index 3dd2e4ecf63e..000000000000 --- a/ui/pages/swaps/dropdown-search-list/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './dropdown-search-list'; diff --git a/ui/pages/swaps/dropdown-search-list/index.scss b/ui/pages/swaps/dropdown-search-list/index.scss deleted file mode 100644 index 904f9530d341..000000000000 --- a/ui/pages/swaps/dropdown-search-list/index.scss +++ /dev/null @@ -1,167 +0,0 @@ -@use "design-system"; - -.dropdown-search-list { - &__search-list-open { - margin: 24px; - box-shadow: none; - border-radius: 6px; - min-height: 297px; - width: 100%; - } - - &__token-container { - margin: 0; - min-height: auto; - border: 1px solid var(--color-border-default); - box-sizing: border-box; - box-shadow: none; - border-radius: 6px; - width: 100%; - } - - &--open { - box-shadow: var(--shadow-size-sm) var(--color-shadow-default); - border: 1px solid var(--color-border-default); - } - - &__close-area { - position: fixed; - top: 0; - left: 0; - z-index: 1000; - width: 100%; - height: 100%; - } - - &__selector-closed-container { - display: flex; - width: 100%; - position: relative; - align-items: center; - max-height: 60px; - transition: 200ms ease-in-out; - border-radius: 6px; - box-shadow: none; - border: 1px solid var(--color-border-default); - height: 60px; - - &:hover { - background: var(--color-background-default-hover); - } - } - - &__caret { - position: absolute; - right: 16px; - color: var(--color-icon-default); - } - - &__selector-closed { - display: flex; - flex-flow: row nowrap; - align-items: center; - padding: 16px 12px; - box-sizing: border-box; - cursor: pointer; - position: relative; - align-items: center; - flex: 1; - height: 60px; - - i { - font-size: 1.2em; - } - - .dropdown-search-list__item-labels { - width: 100%; - } - } - - &__selector-closed-icon { - width: 34px; - height: 34px; - } - - &__closed-primary-label { - @include design-system.H4; - - color: var(--color-text-default); - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__search-list--open { - box-shadow: var(--shadow-size-md) var(--color-shadow-default); - border: 1px solid var(--color-border-muted); - } - - &__default-dropdown-icon { - width: 34px; - height: 34px; - border-radius: 50%; - background: var(--color-background-alternative); - flex: 0 1 auto; - } - - &__labels { - display: flex; - justify-content: space-between; - width: 100%; - flex: 1; - } - - &__item-labels { - display: flex; - flex-direction: column; - justify-content: center; - margin-left: 8px; - } - - &__select-default { - color: var(--color-text-muted); - } - - &__placeholder { - @include design-system.H6; - - padding: 16px; - color: var(--color-text-alternative); - min-height: 300px; - position: relative; - z-index: 1002; - background: var(--color-background-default); - border-radius: 6px; - min-height: 194px; - overflow: hidden; - text-overflow: ellipsis; - - .searchable-item-list__item--add-token { - padding: 8px 0; - } - } - - &__loading-item { - transition: 200ms ease-in-out; - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: center; - padding: 16px 12px; - box-sizing: border-box; - cursor: pointer; - border-top: 1px solid var(--color-border-muted); - position: relative; - z-index: 1; - background: var(--color-background-default); - } - - &__loading-item-text-container { - margin-left: 4px; - } - - &__loading-item-text { - font-weight: bold; - } -} diff --git a/ui/pages/swaps/index.js b/ui/pages/swaps/index.js index 877e38aa7c84..e16166297545 100644 --- a/ui/pages/swaps/index.js +++ b/ui/pages/swaps/index.js @@ -14,7 +14,6 @@ import { Redirect, } from 'react-router-dom'; import { shuffle, isEqual } from 'lodash'; -import classnames from 'classnames'; import { TransactionStatus } from '@metamask/transaction-controller'; import { I18nContext } from '../../contexts/i18n'; @@ -40,25 +39,20 @@ import { prepareToLeaveSwaps, fetchSwapsLivenessAndFeatureFlags, getReviewSwapClickedTimestamp, - getPendingSmartTransactions, getCurrentSmartTransactionsEnabled, getCurrentSmartTransactionsError, - navigateBackToBuildQuote, - getSwapRedesignEnabled, setTransactionSettingsOpened, getLatestAddedTokenTo, } from '../../ducks/swaps/swaps'; import { getCurrentNetworkTransactions } from '../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../shared/modules/selectors'; import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, SMART_TRANSACTION_STATUS_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, DEFAULT_ROUTE, @@ -99,10 +93,8 @@ import AwaitingSignatures from './awaiting-signatures'; import SmartTransactionStatus from './smart-transaction-status'; import AwaitingSwap from './awaiting-swap'; import LoadingQuote from './loading-swaps-quotes'; -import BuildQuote from './build-quote'; import PrepareSwapPage from './prepare-swap-page/prepare-swap-page'; import NotificationPage from './notification-page/notification-page'; -import ViewQuote from './view-quote'; export default function Swap() { const t = useContext(I18nContext); @@ -117,7 +109,6 @@ export default function Swap() { const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE; const isSmartTransactionStatusRoute = pathname === SMART_TRANSACTION_STATUS_ROUTE; - const isViewQuoteRoute = pathname === VIEW_QUOTE_ROUTE; const isPrepareSwapRoute = pathname === PREPARE_SWAP_ROUTE; const [currentStxErrorTracked, setCurrentStxErrorTracked] = useState(false); @@ -140,16 +131,14 @@ export default function Swap() { const tokenList = useSelector(getTokenList, isEqual); const shuffledTokensList = shuffle(Object.values(tokenList)); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); - const pendingSmartTransactions = useSelector(getPendingSmartTransactions); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); - const swapRedesignEnabled = useSelector(getSwapRedesignEnabled); const currentSmartTransactionsError = useSelector( getCurrentSmartTransactionsError, ); @@ -358,167 +347,72 @@ export default function Swap() { <div className="swaps"> <div className="swaps__container"> <div className="swaps__header"> - {!swapRedesignEnabled && ( - <div - className="swaps__header-edit" - onClick={async () => { - await dispatch(navigateBackToBuildQuote(history)); - }} - > - {isViewQuoteRoute && t('edit')} - </div> - )} - {swapRedesignEnabled && ( - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.center} - marginLeft={4} - width={FRACTIONS.ONE_TWELFTH} - tabIndex="0" - onKeyUp={(e) => { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - {!isAwaitingSwapRoute && - !isAwaitingSignaturesRoute && - !isSmartTransactionStatusRoute && ( - <Icon - name={IconName.Arrow2Left} - size={IconSize.Lg} - color={IconColor.iconAlternative} - onClick={redirectToDefaultRoute} - style={{ cursor: 'pointer' }} - title={t('cancel')} - /> - )} - </Box> - )} - <div className="swaps__title">{t('swap')}</div> - {!swapRedesignEnabled && ( - <div - className="swaps__header-cancel" - onClick={async () => { - clearTemporaryTokenRef.current(); - dispatch(clearSwapsState()); - await dispatch(resetBackgroundSwapsState()); - history.push(DEFAULT_ROUTE); - }} - > - {!isAwaitingSwapRoute && - !isAwaitingSignaturesRoute && - !isSmartTransactionStatusRoute && - t('cancel')} - </div> - )} - {swapRedesignEnabled && ( - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.center} - marginRight={4} - width={FRACTIONS.ONE_TWELFTH} - tabIndex="0" - onKeyUp={(e) => { - if (e.key === 'Enter') { - dispatch(setTransactionSettingsOpened(true)); - } - }} - > - {isPrepareSwapRoute && ( + <Box + display={DISPLAY.FLEX} + justifyContent={JustifyContent.center} + marginLeft={4} + width={FRACTIONS.ONE_TWELFTH} + tabIndex="0" + onKeyUp={(e) => { + if (e.key === 'Enter') { + redirectToDefaultRoute(); + } + }} + > + {!isAwaitingSwapRoute && + !isAwaitingSignaturesRoute && + !isSmartTransactionStatusRoute && ( <Icon - name={IconName.Setting} + name={IconName.Arrow2Left} size={IconSize.Lg} color={IconColor.iconAlternative} - onClick={() => { - dispatch(setTransactionSettingsOpened(true)); - }} + onClick={redirectToDefaultRoute} style={{ cursor: 'pointer' }} - title={t('transactionSettings')} + title={t('cancel')} /> )} - </Box> - )} + </Box> + <div className="swaps__title">{t('swap')}</div> + <Box + display={DISPLAY.FLEX} + justifyContent={JustifyContent.center} + marginRight={4} + width={FRACTIONS.ONE_TWELFTH} + tabIndex="0" + onKeyUp={(e) => { + if (e.key === 'Enter') { + dispatch(setTransactionSettingsOpened(true)); + } + }} + > + {isPrepareSwapRoute && ( + <Icon + name={IconName.Setting} + size={IconSize.Lg} + color={IconColor.iconAlternative} + onClick={() => { + dispatch(setTransactionSettingsOpened(true)); + }} + style={{ cursor: 'pointer' }} + title={t('transactionSettings')} + /> + )} + </Box> </div> - <div - className={classnames('swaps__content', { - 'swaps__content--redesign-enabled': swapRedesignEnabled, - })} - > + <div className="swaps__content"> <Switch> - <FeatureToggledRoute - redirectRoute={SWAPS_MAINTENANCE_ROUTE} - flag={swapsEnabled} - path={BUILD_QUOTE_ROUTE} - exact - render={() => { - if (swapRedesignEnabled) { - return <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} />; - } - if (tradeTxData && !conversionError) { - return <Redirect to={{ pathname: AWAITING_SWAP_ROUTE }} />; - } else if (tradeTxData && routeState) { - return <Redirect to={{ pathname: SWAPS_ERROR_ROUTE }} />; - } else if (routeState === 'loading' && aggregatorMetadata) { - return <Redirect to={{ pathname: LOADING_QUOTES_ROUTE }} />; - } - - return ( - <BuildQuote - ethBalance={ethBalance} - selectedAccountAddress={selectedAccountAddress} - shuffledTokensList={shuffledTokensList} - /> - ); - }} - /> <FeatureToggledRoute redirectRoute={SWAPS_MAINTENANCE_ROUTE} flag={swapsEnabled} path={PREPARE_SWAP_ROUTE} exact - render={() => { - if (!swapRedesignEnabled) { - return <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} />; - } - - return ( - <PrepareSwapPage - ethBalance={ethBalance} - selectedAccountAddress={selectedAccountAddress} - shuffledTokensList={shuffledTokensList} - /> - ); - }} - /> - <FeatureToggledRoute - redirectRoute={SWAPS_MAINTENANCE_ROUTE} - flag={swapsEnabled} - path={VIEW_QUOTE_ROUTE} - exact - render={() => { - if ( - pendingSmartTransactions.length > 0 && - routeState === 'smartTransactionStatus' - ) { - return ( - <Redirect - to={{ pathname: SMART_TRANSACTION_STATUS_ROUTE }} - /> - ); - } - if (swapRedesignEnabled) { - return <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} />; - } - if (Object.values(quotes).length) { - return ( - <ViewQuote numberOfQuotes={Object.values(quotes).length} /> - ); - } else if (fetchParams) { - return <Redirect to={{ pathname: SWAPS_ERROR_ROUTE }} />; - } - return <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} />; - }} + render={() => ( + <PrepareSwapPage + ethBalance={ethBalance} + selectedAccountAddress={selectedAccountAddress} + shuffledTokensList={shuffledTokensList} + /> + )} /> <Route path={SWAPS_ERROR_ROUTE} @@ -535,7 +429,7 @@ export default function Swap() { /> ); } - return <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} />; + return <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} />; }} /> <Route @@ -568,13 +462,13 @@ export default function Swap() { dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)); history.push(SWAPS_ERROR_ROUTE); } else { - history.push(VIEW_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } }} aggregatorMetadata={aggregatorMetadata} /> ) : ( - <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} /> + <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} /> ); }} /> @@ -585,7 +479,7 @@ export default function Swap() { return swapsEnabled === false ? ( <AwaitingSwap errorKey={OFFLINE_FOR_MAINTENANCE} /> ) : ( - <Redirect to={{ pathname: BUILD_QUOTE_ROUTE }} /> + <Redirect to={{ pathname: PREPARE_SWAP_ROUTE }} /> ); }} /> diff --git a/ui/pages/swaps/index.scss b/ui/pages/swaps/index.scss index 33fc1e9a17cb..4de2ac35707f 100644 --- a/ui/pages/swaps/index.scss +++ b/ui/pages/swaps/index.scss @@ -3,27 +3,21 @@ @import 'awaiting-swap/index'; @import 'awaiting-signatures/index'; @import 'smart-transaction-status/index'; -@import 'build-quote/index'; @import 'prepare-swap-page/index'; @import 'notification-page/index'; @import 'countdown-timer/index'; -@import 'dropdown-input-pair/index'; -@import 'dropdown-search-list/index'; @import 'exchange-rate-display/index'; @import 'fee-card/index'; @import 'loading-swaps-quotes/index'; -@import 'main-quote-summary/index'; @import 'searchable-item-list/index'; @import 'select-quote-popover/index'; -@import 'slippage-buttons/index'; @import 'swaps-footer/index'; -@import 'view-quote/index'; @import 'create-new-swap/index'; @import 'view-on-block-explorer/index'; @import 'transaction-settings/index'; @import 'list-with-search/index'; -@import 'popover-custom-background/index'; @import 'mascot-background-animation/index'; +@import 'selected-token/index'; .swaps { display: flex; @@ -78,13 +72,7 @@ } @include design-system.screen-sm-min { - width: 348px; - } - - &--redesign-enabled { - @include design-system.screen-sm-min { - width: 100%; - } + width: 100%; } } @@ -119,25 +107,6 @@ } } - &__header-cancel { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - padding-right: 24px; - flex: 1; - text-align: right; - } - - &__header-edit { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - padding-left: 24px; - flex: 1; - } - .actionable-message__message &__notification-close-button { background-color: transparent; position: absolute; diff --git a/ui/pages/swaps/index.test.js b/ui/pages/swaps/index.test.js index aa4109567f99..4157b614562f 100644 --- a/ui/pages/swaps/index.test.js +++ b/ui/pages/swaps/index.test.js @@ -22,7 +22,7 @@ jest.mock('react-router-dom', () => ({ }), useLocation: jest.fn(() => { return { - pathname: '/swaps/build-quote', + pathname: '/swaps/prepare-swap-page', }; }), })); @@ -81,12 +81,10 @@ describe('Swap', () => { it('renders the component with initial props', async () => { const swapsMockStore = createSwapsMockStore(); - swapsMockStore.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = false; const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider(<Swap />, store); await waitFor(() => expect(featureFlagsNock.isDone()).toBe(true)); expect(getByText('Swap')).toBeInTheDocument(); - expect(getByText('Cancel')).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js index a8b7c9bf2a51..ebbb6f652496 100644 --- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js +++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js @@ -6,7 +6,7 @@ import { shuffle } from 'lodash'; import { useHistory } from 'react-router-dom'; import isEqual from 'lodash/isEqual'; import { - navigateBackToBuildQuote, + navigateBackToPrepareSwap, getFetchParams, getQuotesFetchStartTime, getCurrentSmartTransactionsEnabled, @@ -16,8 +16,8 @@ import { getHardwareWalletType, } from '../../../selectors/selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { I18nContext } from '../../../contexts/i18n'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -51,7 +51,7 @@ export default function LoadingSwapsQuotes({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( @@ -183,7 +183,7 @@ export default function LoadingSwapsQuotes({ submitText={t('back')} onSubmit={async () => { trackEvent(quotesRequestCancelledEventConfig); - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); }} hideCancel /> diff --git a/ui/pages/swaps/main-quote-summary/README.mdx b/ui/pages/swaps/main-quote-summary/README.mdx deleted file mode 100644 index c32397d1e762..000000000000 --- a/ui/pages/swaps/main-quote-summary/README.mdx +++ /dev/null @@ -1,14 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; -import MainQuoteSummary from '.'; - -# MainQuoteSummary - -MainQuoteSummary displays the quote of a swap. - -<Canvas> - <Story id="pages-swaps-mainquotesummary--default-story" /> -</Canvas> - -## Props - -<ArgsTable of={MainQuoteSummary} /> \ No newline at end of file diff --git a/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap b/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap deleted file mode 100644 index 3b202988fc42..000000000000 --- a/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MainQuoteSummary renders the component with initial props 1`] = ` -<div - class="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" -> - <span - class="main-quote-summary__source-row-value" - title="2" - > - 2 - </span> - <div - class="" - > - <span - class="icon-with-fallback__fallback url-icon__fallback main-quote-summary__icon-fallback" - > - E - </span> - </div> - <span - class="main-quote-summary__source-row-symbol" - title="ETH" - > - ETH - </span> -</div> -`; - -exports[`MainQuoteSummary renders the component with initial props 2`] = ` -<div - class="main-quote-summary__destination-row" -> - <div - class="" - > - <span - class="icon-with-fallback__fallback url-icon__fallback main-quote-summary__icon-fallback" - > - B - </span> - </div> - <span - class="main-quote-summary__destination-row-symbol" - > - BAT - </span> -</div> -`; - -exports[`MainQuoteSummary renders the component with initial props 3`] = ` -<div - class="main-quote-summary__quote-large" -> - <div> - <div - class="" - style="display: inline;" - tabindex="0" - title="" - > - <span - class="main-quote-summary__quote-large-number" - style="font-size: 50px; line-height: 48px;" - > - 0.2 - </span> - </div> - </div> -</div> -`; - -exports[`MainQuoteSummary renders the component with initial props 4`] = ` -<div - class="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" -> - <div - class="exchange-rate-display main-quote-summary__exchange-rate-display" - > - <div - class="box exchange-rate-display__quote-rate--no-link box--display-flex box--gap-1 box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default" - data-testid="exchange-rate-display-quote-rate" - > - <span> - 1 - </span> - <span - class="" - data-testid="exchange-rate-display-base-symbol" - > - ETH - </span> - <span> - = - </span> - <span> - 0.1 - </span> - <span - class="" - > - BAT - </span> - </div> - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative" - data-testid="exchange-rate-display-switch" - style="mask-image: url('./images/icons/swap-horizontal.svg'); cursor: pointer;" - title="Switch" - /> - </div> -</div> -`; diff --git a/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap b/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap deleted file mode 100644 index 2a1fef0ff0ac..000000000000 --- a/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap +++ /dev/null @@ -1,74 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`QuotesBackdrop renders the component with initial props 1`] = ` -<g - filter="url(#filter0_d)" -> - <path - d="M25.4749 54C25.4749 49.5817 29.0566 46 33.4749 46H328.475C332.893 46 336.475 49.5817 336.475 54V185.5C336.475 189.918 332.893 193.5 328.475 193.5H33.4749C29.0566 193.5 25.4749 189.918 25.4749 185.5V54Z" - fill="url(#paint0_linear)" - /> -</g> -`; - -exports[`QuotesBackdrop renders the component with initial props 2`] = ` -<filter - color-interpolation-filters="sRGB" - filterUnits="userSpaceOnUse" - height="242.164" - id="filter0_d" - width="389" - x="-13.5251" - y="0.335938" -> - <feflood - flood-opacity="0" - result="BackgroundImageFix" - /> - <fecolormatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - /> - <feoffset - dy="10" - /> - <fegaussianblur - stdDeviation="19.5" - /> - <fecolormatrix - type="matrix" - values="0 0 0 0 0.0117647 0 0 0 0 0.491686 0 0 0 0 0.839216 0 0 0 0.15 0" - /> - <feblend - in2="BackgroundImageFix" - mode="normal" - result="effect1_dropShadow" - /> - <feblend - in="SourceGraphic" - in2="effect1_dropShadow" - mode="normal" - result="shape" - /> -</filter> -`; - -exports[`QuotesBackdrop renders the component with initial props 3`] = ` -<lineargradient - gradientUnits="userSpaceOnUse" - id="paint0_linear" - x1="25.4749" - x2="342.234" - y1="90.693" - y2="90.693" -> - <stop - stop-color="#037DD6" - /> - <stop - offset="0.994792" - stop-color="#1098FC" - /> -</lineargradient> -`; diff --git a/ui/pages/swaps/main-quote-summary/index.js b/ui/pages/swaps/main-quote-summary/index.js deleted file mode 100644 index 235070e29323..000000000000 --- a/ui/pages/swaps/main-quote-summary/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './main-quote-summary'; diff --git a/ui/pages/swaps/main-quote-summary/index.scss b/ui/pages/swaps/main-quote-summary/index.scss deleted file mode 100644 index 3f7693705db8..000000000000 --- a/ui/pages/swaps/main-quote-summary/index.scss +++ /dev/null @@ -1,125 +0,0 @@ -@use "design-system"; - -.main-quote-summary { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - position: relative; - width: 100%; - color: var(--color-text-default); - margin-top: 28px; - margin-bottom: 56px; - - &__source-row, - &__destination-row { - width: 100%; - display: flex; - align-items: flex-start; - justify-content: center; - - @include design-system.H6; - - color: var(--color-text-alternative); - } - - &__source-row { - align-items: center; - } - - &__source-row-value, - &__source-row-symbol { - // Each of these spans can be half their container width minus the space - // needed for the token icon and the span margins - max-width: calc(50% - 13px); - } - - - &__source-row-value { - margin-right: 5px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__source-row-symbol { - margin-left: 5px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__destination-row { - margin-top: 6px; - } - - &__destination-row-symbol { - margin-left: 5px; - color: var(--color-text-default); - } - - &__icon, - &__icon-fallback { - height: 16px; - width: 16px; - } - - &__icon-fallback { - padding-top: 0; - font-size: 12px; - line-height: 16px; - } - - &__down-arrow { - margin-top: 5px; - color: var(--color-icon-muted); - } - - &__details { - display: flex; - flex-flow: column; - align-items: center; - width: 310px; - position: relative; - } - - &__quote-details-top { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - width: 100%; - } - - &__quote-large { - display: flex; - align-items: flex-start; - margin-top: 8px; - height: 50px; - } - - &__quote-large-number { - font-size: 50px; - line-height: 48px; - } - - &__quote-large-white { - font-size: 40px; - text-overflow: ellipsis; - width: 295px; - overflow: hidden; - white-space: nowrap; - } - - &__exchange-rate-container { - display: flex; - justify-content: center; - align-items: center; - width: 287px; - margin-top: 14px; - } - - &__exchange-rate-display { - color: var(--color-text-alternative); - } -} diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.js deleted file mode 100644 index d7ff9646a3a6..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.js +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BigNumber from 'bignumber.js'; -import Tooltip from '../../../components/ui/tooltip'; -import UrlIcon from '../../../components/ui/url-icon'; -import ExchangeRateDisplay from '../exchange-rate-display'; -import { formatSwapsValueForDisplay } from '../swaps.util'; -import { - calcTokenAmount, - toPrecisionWithoutTrailingZeros, -} from '../../../../shared/lib/transactions-controller-utils'; - -function getFontSizesAndLineHeights(fontSizeScore) { - if (fontSizeScore <= 9) { - return [50, 48]; - } - if (fontSizeScore <= 13) { - return [40, 32]; - } - return [26, 15]; -} - -export default function MainQuoteSummary({ - sourceValue, - sourceSymbol, - sourceDecimals, - sourceIconUrl, - destinationValue, - destinationSymbol, - destinationDecimals, - destinationIconUrl, -}) { - const sourceAmount = toPrecisionWithoutTrailingZeros( - calcTokenAmount(sourceValue, sourceDecimals).toString(10), - 12, - ); - const destinationAmount = calcTokenAmount( - destinationValue, - destinationDecimals, - ); - - const amountToDisplay = formatSwapsValueForDisplay(destinationAmount); - const amountDigitLength = amountToDisplay.match(/\d+/gu).join('').length; - const [numberFontSize, lineHeight] = - getFontSizesAndLineHeights(amountDigitLength); - let ellipsedAmountToDisplay = amountToDisplay; - - if (amountDigitLength > 20) { - ellipsedAmountToDisplay = `${amountToDisplay.slice(0, 20)}...`; - } - - return ( - <div className="main-quote-summary"> - <div className="main-quote-summary__details"> - <div className="main-quote-summary__quote-details-top"> - <div - className="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" - > - <span - className="main-quote-summary__source-row-value" - title={formatSwapsValueForDisplay(sourceAmount)} - > - {formatSwapsValueForDisplay(sourceAmount)} - </span> - <UrlIcon - url={sourceIconUrl} - className="main-quote-summary__icon" - name={sourceSymbol} - fallbackClassName="main-quote-summary__icon-fallback" - /> - <span - className="main-quote-summary__source-row-symbol" - title={sourceSymbol} - > - {sourceSymbol} - </span> - </div> - <i className="fa fa-arrow-down main-quote-summary__down-arrow" /> - <div className="main-quote-summary__destination-row"> - <UrlIcon - url={destinationIconUrl} - className="main-quote-summary__icon" - name={destinationSymbol} - fallbackClassName="main-quote-summary__icon-fallback" - /> - <span className="main-quote-summary__destination-row-symbol"> - {destinationSymbol} - </span> - </div> - <div className="main-quote-summary__quote-large"> - <Tooltip - interactive - position="bottom" - html={amountToDisplay} - disabled={ellipsedAmountToDisplay === amountToDisplay} - > - <span - className="main-quote-summary__quote-large-number" - style={{ - fontSize: numberFontSize, - lineHeight: `${lineHeight}px`, - }} - > - {`${ellipsedAmountToDisplay}`} - </span> - </Tooltip> - </div> - </div> - <div - className="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" - > - <ExchangeRateDisplay - primaryTokenValue={sourceValue} - primaryTokenDecimals={sourceDecimals} - primaryTokenSymbol={sourceSymbol} - secondaryTokenValue={destinationValue} - secondaryTokenDecimals={destinationDecimals} - secondaryTokenSymbol={destinationSymbol} - arrowColor="var(--color-primary-default)" - boldSymbols={false} - className="main-quote-summary__exchange-rate-display" - /> - </div> - </div> - </div> - ); -} - -MainQuoteSummary.propTypes = { - /** - * The amount that will be sent in the smallest denomination. - * For example, wei is the smallest denomination for ether. - */ - sourceValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(BigNumber), - ]).isRequired, - - /** - * Maximum number of decimal places for the source token. - */ - sourceDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** - * The ticker symbol for the source token. - */ - sourceSymbol: PropTypes.string.isRequired, - - /** - * The amount that will be received in the smallest denomination. - * For example, wei is the smallest denomination for ether. - */ - destinationValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(BigNumber), - ]).isRequired, - - /** - * Maximum number of decimal places for the destination token. - */ - destinationDecimals: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - - /** - * The ticker symbol for the destination token. - */ - destinationSymbol: PropTypes.string.isRequired, - - /** - * The location of the source token icon file. - */ - sourceIconUrl: PropTypes.string, - - /** - * The location of the destination token icon file. - */ - destinationIconUrl: PropTypes.string, -}; diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js deleted file mode 100644 index 56ed74624193..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import README from './README.mdx'; -import MainQuoteSummary from './main-quote-summary'; - -export default { - title: 'Pages/Swaps/MainQuoteSummary', - - component: MainQuoteSummary, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - sourceValue: { - control: 'text', - }, - sourceDecimals: { - control: 'number', - }, - sourceSymbol: { - control: 'text', - }, - destinationValue: { - control: 'text', - }, - destinationDecimals: { - control: 'number', - }, - destinationSymbol: { - control: 'text', - }, - sourceIconUrl: { - control: 'text', - }, - destinationIconUrl: { - control: 'text', - }, - }, - args: { - sourceValue: '2000000000000000000', - sourceDecimals: 18, - sourceSymbol: 'ETH', - destinationValue: '200000000000000000', - destinationDecimals: 18, - destinationSymbol: 'ABC', - sourceIconUrl: '.storybook/images/metamark.svg', - destinationIconUrl: '.storybook/images/sai.svg', - }, -}; - -export const DefaultStory = (args) => { - return ( - <div - style={{ - width: '360px', - height: '224px', - border: '1px solid black', - padding: '24px', - }} - > - <MainQuoteSummary {...args} /> - </div> - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js deleted file mode 100644 index 85e17bd48de4..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../test/jest'; -import MainQuoteSummary from '.'; - -const createProps = (customProps = {}) => { - return { - sourceValue: '2000000000000000000', - sourceDecimals: 18, - sourceSymbol: 'ETH', - destinationValue: '200000000000000000', - destinationDecimals: 18, - destinationSymbol: 'BAT', - ...customProps, - }; -}; - -describe('MainQuoteSummary', () => { - it('renders the component with initial props', () => { - const props = createProps(); - const { getAllByText } = renderWithProvider( - <MainQuoteSummary {...props} />, - ); - expect(getAllByText(props.sourceSymbol)).toHaveLength(2); - expect(getAllByText(props.destinationSymbol)).toHaveLength(2); - expect( - document.querySelector('.main-quote-summary__source-row'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__destination-row'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__quote-large'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.js deleted file mode 100644 index 44351c73bb5d..000000000000 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.js +++ /dev/null @@ -1,89 +0,0 @@ -/* eslint-disable @metamask/design-tokens/color-no-hex*/ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default function QuotesBackdrop({ withTopTab }) { - return ( - <svg - width="311" - height="164" - viewBox="25.5 29.335899353027344 311 164" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <g filter="url(#filter0_d)"> - <path - d="M25.4749 54C25.4749 49.5817 29.0566 46 33.4749 46H328.475C332.893 46 336.475 49.5817 336.475 54V185.5C336.475 189.918 332.893 193.5 328.475 193.5H33.4749C29.0566 193.5 25.4749 189.918 25.4749 185.5V54Z" - fill="url(#paint0_linear)" - /> - {withTopTab && ( - <path - d="M132.68 34.3305C133.903 31.3114 136.836 29.3359 140.094 29.3359H219.858C223.116 29.3359 226.048 31.3114 227.272 34.3305L237.443 59.4217C239.575 64.6815 235.705 70.4271 230.029 70.4271H129.922C124.247 70.4271 120.376 64.6814 122.508 59.4217L132.68 34.3305Z" - fill="url(#paint1_linear)" - /> - )} - </g> - <defs> - <filter - id="filter0_d" - x="-13.5251" - y="0.335938" - width="389" - height="242.164" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - /> - <feOffset dy="10" /> - <feGaussianBlur stdDeviation="19.5" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0.0117647 0 0 0 0 0.491686 0 0 0 0 0.839216 0 0 0 0.15 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow" - result="shape" - /> - </filter> - <linearGradient - id="paint0_linear" - x1="25.4749" - y1="90.693" - x2="342.234" - y2="90.693" - gradientUnits="userSpaceOnUse" - > - <stop stopColor="#037DD6" /> - <stop offset="0.994792" stopColor="#1098FC" /> - </linearGradient> - <linearGradient - id="paint1_linear" - x1="25.4749" - y1="90.693" - x2="342.234" - y2="90.693" - gradientUnits="userSpaceOnUse" - > - <stop stopColor="#037DD6" /> - <stop offset="0.994792" stopColor="#1098FC" /> - </linearGradient> - </defs> - </svg> - ); -} - -QuotesBackdrop.propTypes = { - withTopTab: PropTypes.bool, -}; diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js deleted file mode 100644 index 00d23c2656d6..000000000000 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../test/jest'; -import QuotesBackdrop from './quote-backdrop'; - -const createProps = (customProps = {}) => { - return { - withTopTab: false, - ...customProps, - }; -}; - -describe('QuotesBackdrop', () => { - it('renders the component with initial props', () => { - const { container } = renderWithProvider( - <QuotesBackdrop {...createProps()} />, - ); - expect(container.firstChild.nodeName).toBe('svg'); - expect(document.querySelector('g')).toMatchSnapshot(); - expect(document.querySelector('filter')).toMatchSnapshot(); - expect(document.querySelector('linearGradient')).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/popover-custom-background/index.scss b/ui/pages/swaps/popover-custom-background/index.scss deleted file mode 100644 index 07bc852edbfd..000000000000 --- a/ui/pages/swaps/popover-custom-background/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.popover-custom-background { - height: 100%; - width: 100%; - background: var(--color-background-alternative); - opacity: 0.6; -} diff --git a/ui/pages/swaps/popover-custom-background/popover-custom-background.js b/ui/pages/swaps/popover-custom-background/popover-custom-background.js deleted file mode 100644 index 8e8af648ad7f..000000000000 --- a/ui/pages/swaps/popover-custom-background/popover-custom-background.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Box from '../../../components/ui/box'; - -const PopoverCustomBackground = ({ onClose }) => { - return <Box className="popover-custom-background" onClick={onClose}></Box>; -}; - -export default PopoverCustomBackground; - -PopoverCustomBackground.propTypes = { - onClose: PropTypes.func, -}; diff --git a/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap b/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap deleted file mode 100644 index 8991347eecfc..000000000000 --- a/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PrepareSwapPage renders the component with initial props 1`] = `null`; diff --git a/ui/pages/swaps/prepare-swap-page/index.scss b/ui/pages/swaps/prepare-swap-page/index.scss index 1909f11024b1..b443006330d3 100644 --- a/ui/pages/swaps/prepare-swap-page/index.scss +++ b/ui/pages/swaps/prepare-swap-page/index.scss @@ -27,12 +27,6 @@ margin-top: 16px; position: relative; - .dropdown-input-pair__input { - input { - text-align: right; - } - } - .MuiInputBase-root { padding-right: 0; } @@ -124,98 +118,6 @@ } } - .dropdown-search-list { - background-color: var(--color-background-alternative); - border-radius: 100px; - - &__select-default { - color: var(--color-text-default); - } - - &__labels { - flex: auto; - max-width: 110px; - - &--with-icon { - max-width: 95px; - } - } - - &__closed-primary-label { - font-weight: 500; - } - - &__selector-closed-container { - border: 0; - border-radius: 100px; - height: 32px; - max-height: 32px; - max-width: 165px; - width: auto; - } - - &__selector-closed-icon { - width: 24px; - height: 24px; - margin-right: 8px; - } - - &__selector-closed { - height: 32px; - max-width: 140px; - - div { - display: flex; - } - - &__item-labels { - width: 100%; - margin-left: 0; - } - } - } - - .dropdown-input-pair { - height: 32px; - width: auto; - - &__selector--closed { - height: 32px; - border-top-right-radius: 100px; - border-bottom-right-radius: 100px; - } - - .searchable-item-list { - &__item--add-token { - display: none; - } - } - - &__to { - display: flex; - justify-content: space-between; - align-items: center; - - .searchable-item-list { - &__item--add-token { - display: flex; - } - } - } - - &__input { - div { - border: 0; - } - } - - &__two-line-input { - input { - padding-bottom: 0; - } - } - } - &__token-etherscan-link { color: var(--color-primary-default); cursor: pointer; @@ -306,12 +208,6 @@ width: 100%; } - .main-quote-summary { - &__exchange-rate-display { - width: auto; - } - } - &::after { // Hide preloaded images. position: absolute; width: 0; @@ -365,6 +261,10 @@ &__edit-limit { white-space: nowrap; } + + &__exchange-rate-display { + width: auto !important; + } } @keyframes slide-in { 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 45120d9f6a6b..8a701289bebd 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -69,8 +69,9 @@ import { getDataCollectionForMarketing, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsPreferenceEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { getValueFromWeiHex, @@ -212,14 +213,15 @@ export default function PrepareSwapPage({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); const isSmartTransaction = - currentSmartTransactionsEnabled && smartTransactionsOptInStatus; + useSelector(getSmartTransactionsPreferenceEnabled) && + currentSmartTransactionsEnabled; const currentCurrency = useSelector(getCurrentCurrency); const fetchingQuotes = useSelector(getFetchingQuotes); const loadingComplete = !fetchingQuotes && areQuotesPresent; @@ -784,10 +786,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/prepare-swap-page.test.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js index d576dad0933d..a76160ef77bb 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js @@ -82,9 +82,6 @@ describe('PrepareSwapPage', () => { store, ); expect(getByText('Select token')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); }); it('switches swap from and to tokens', () => { diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 496ae5ee6d9e..13d11a93cd1f 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, @@ -62,8 +58,9 @@ import { getUSDConversionRate, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, getSmartTransactionsEnabled, + getSmartTransactionsPreferenceEnabled, } from '../../../../shared/modules/selectors'; import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; import { @@ -79,11 +76,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 +87,7 @@ import { getRenderableNetworkFeesForQuote, getFeeForSmartTransaction, formatSwapsValueForDisplay, + getSwap1559GasFeeEstimates, } from '../swaps.util'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { @@ -113,6 +110,7 @@ import { Size, FlexDirection, Severity, + FontStyle, } from '../../../helpers/constants/design-system'; import { BannerAlert, @@ -143,11 +141,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 +220,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,17 +232,19 @@ 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); const nativeCurrencySymbol = useSelector(getNativeCurrency); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, + getSmartTransactionsOptInStatusForMetrics, + ); + const smartTransactionsPreferenceEnabled = useSelector( + getSmartTransactionsPreferenceEnabled, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const swapsSTXLoading = useSelector(getSwapsSTXLoading); @@ -228,9 +257,35 @@ 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; + useSelector(getSmartTransactionsPreferenceEnabled) && + currentSmartTransactionsEnabled; const [slippageErrorKey] = useState(() => { const slippage = Number(fetchParams?.slippage); @@ -242,15 +297,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 +321,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 +362,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, @@ -345,16 +382,15 @@ export default function ReviewQuote({ setReceiveToAmount }) { chainId, smartTransactionEstimatedGas: smartTransactionsEnabled && - smartTransactionsOptInStatus && + smartTransactionsPreferenceEnabled && smartTransactionFees?.tradeTxFees, nativeCurrencySymbol, multiLayerL1ApprovalFeeTotal, }); }, [ quotes, - gasPrice, - baseAndPriorityFeePerGas, - networkAndAccountSupports1559, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -363,7 +399,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { smartTransactionFees?.tradeTxFees, nativeCurrencySymbol, smartTransactionsEnabled, - smartTransactionsOptInStatus, + smartTransactionsPreferenceEnabled, multiLayerL1ApprovalFeeTotal, ]); @@ -385,9 +421,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { getRenderableNetworkFeesForQuote({ tradeGas: usedGasLimit, approveGas, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -404,7 +439,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { const renderableMaxFees = getRenderableNetworkFeesForQuote({ tradeGas: maxGasLimit, approveGas, - gasPrice: maxFeePerGas || gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -850,13 +886,13 @@ export default function ReviewQuote({ setReceiveToAmount }) { tokenBalanceUnavailable || disableSubmissionDueToPriceWarning || (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || + gasFeeEstimatesTrade?.baseAndPriorityFeePerGas === undefined) || (!networkAndAccountSupports1559 && (gasPrice === null || gasPrice === undefined)) || (currentSmartTransactionsEnabled && (currentSmartTransactionsError || smartTransactionsError)) || (currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && + smartTransactionsPreferenceEnabled && !smartTransactionFees?.tradeTxFees), ); @@ -880,7 +916,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 +961,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { chainId, swapsNetworkConfig.stxGetTransactionsRefreshTime, insufficientTokens, + isGasIncludedTrade, ]); useEffect(() => { @@ -1045,6 +1084,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"> @@ -1068,7 +1141,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { initialAggId={usedQuote.aggregator} onQuoteDetailsIsOpened={trackQuoteDetailsOpened} hideEstimatedGasFee={ - smartTransactionsEnabled && smartTransactionsOptInStatus + smartTransactionsEnabled && smartTransactionsPreferenceEnabled } /> ) @@ -1122,9 +1195,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( @@ -1137,82 +1210,160 @@ export default function ReviewQuote({ setReceiveToAmount }) { secondaryTokenDecimals={destinationTokenDecimals} secondaryTokenSymbol={destinationTokenSymbol} boldSymbols={false} - className="main-quote-summary__exchange-rate-display" + className="review-quote__exchange-rate-display" showIconForSwappingTokens={false} /> </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 +1373,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 +1382,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 +1399,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 +1415,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/select-quote-popover/quote-details/index.scss b/ui/pages/swaps/select-quote-popover/quote-details/index.scss index 861759235aba..80e034ab1d4d 100644 --- a/ui/pages/swaps/select-quote-popover/quote-details/index.scss +++ b/ui/pages/swaps/select-quote-popover/quote-details/index.scss @@ -30,10 +30,6 @@ align-items: center; height: inherit; - .view-quote__conversion-rate-eth-label { - color: var(--color-text-default); - } - i { color: var(--color-primary-default); } @@ -68,14 +64,6 @@ } } - .view-quote__conversion-rate-token-label { - @include design-system.H6; - - color: var(--color-text-default); - font-weight: bold; - margin-left: 2px; - } - &__metafox-logo { width: 17px; margin-right: 4px; diff --git a/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap b/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap index 11cd9372ed7f..189eda3e38e3 100644 --- a/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap +++ b/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap @@ -3,40 +3,44 @@ exports[`SelectedToken renders the component with initial props 1`] = ` <div> <div - class="dropdown-search-list dropdown-search-list__selector-closed-container dropdown-input-pair__selector--closed" - data-testid="dropdown-search-list" - tabindex="0" + class="selected-token" > <div - class="dropdown-search-list__selector-closed" + class="selected-token-list selected-token-list__selector-closed-container selected-token-input-pair__selector--closed" + data-testid="selected-token-list" + tabindex="0" > <div - class="" - > - <img - alt="ETH" - class="url-icon dropdown-search-list__selector-closed-icon" - src="iconUrl" - /> - </div> - <div - class="dropdown-search-list__labels dropdown-search-list__labels--with-icon" + class="selected-token-list__selector-closed" > <div - class="dropdown-search-list__item-labels" + class="" + > + <img + alt="ETH" + class="url-icon selected-token-list__selector-closed-icon" + src="iconUrl" + /> + </div> + <div + class="selected-token-list__labels selected-token-list__labels--with-icon" > - <span - class="dropdown-search-list__closed-primary-label" + <div + class="selected-token-list__item-labels" > - ETH - </span> + <span + class="selected-token-list__closed-primary-label" + > + ETH + </span> + </div> </div> </div> + <span + class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> </div> - <span - class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" - style="mask-image: url('./images/icons/arrow-down.svg');" - /> </div> </div> `; @@ -44,31 +48,35 @@ exports[`SelectedToken renders the component with initial props 1`] = ` exports[`SelectedToken renders the component with no token selected 1`] = ` <div> <div - class="dropdown-search-list dropdown-search-list__selector-closed-container dropdown-input-pair__selector--closed" - data-testid="dropdown-search-list" - tabindex="0" + class="selected-token" > <div - class="dropdown-search-list__selector-closed" + class="selected-token-list selected-token-list__selector-closed-container selected-token-input-pair__selector--closed" + data-testid="selected-token-list" + tabindex="0" > <div - class="dropdown-search-list__labels" + class="selected-token-list__selector-closed" > <div - class="dropdown-search-list__item-labels" + class="selected-token-list__labels" > - <span - class="dropdown-search-list__closed-primary-label dropdown-search-list__select-default" + <div + class="selected-token-list__item-labels" > - Select token - </span> + <span + class="selected-token-list__closed-primary-label selected-token-list__select-default" + > + Select token + </span> + </div> </div> </div> + <span + class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" + style="mask-image: url('./images/icons/arrow-down.svg');" + /> </div> - <span - class="mm-box mm-icon mm-icon--size-xs mm-box--margin-right-3 mm-box--display-inline-block mm-box--color-icon-alternative" - style="mask-image: url('./images/icons/arrow-down.svg');" - /> </div> </div> `; diff --git a/ui/pages/swaps/selected-token/index.scss b/ui/pages/swaps/selected-token/index.scss new file mode 100644 index 000000000000..bc69a934ae02 --- /dev/null +++ b/ui/pages/swaps/selected-token/index.scss @@ -0,0 +1,142 @@ +@use "design-system"; + +.selected-token { + .selected-token-list { + background-color: var(--color-background-alternative); + border-radius: 100px; + + &__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + } + + &__select-default { + color: var(--color-text-default); + } + + &__labels { + display: flex; + justify-content: space-between; + width: 100%; + flex: auto; + max-width: 110px; + + &--with-icon { + max-width: 95px; + } + } + + &__closed-primary-label { + @include design-system.H4; + + color: var(--color-text-default); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + } + + &__selector-closed-container { + display: flex; + position: relative; + align-items: center; + transition: 200ms ease-in-out; + box-shadow: none; + border: 0; + border-radius: 100px; + height: 32px; + max-height: 32px; + max-width: 165px; + width: auto; + + &:hover { + background: var(--color-background-default-hover); + } + } + + &__selector-closed-icon { + width: 24px; + height: 24px; + margin-right: 8px; + } + + &__selector-closed { + display: flex; + flex-flow: row nowrap; + padding: 16px 12px; + box-sizing: border-box; + cursor: pointer; + position: relative; + align-items: center; + flex: 1; + height: 32px; + max-width: 140px; + + i { + font-size: 1.2em; + } + + div { + display: flex; + } + + &__item-labels { + width: 100%; + margin-left: 0; + } + } + + &__item-labels { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + } + } + + .selected-token-input-pair { + height: 32px; + width: auto; + + &__selector--closed { + height: 60px; + border-top-right-radius: 100px; + border-bottom-right-radius: 100px; + } + + .searchable-item-list { + &__item--add-token { + display: none; + } + } + + &__to { + display: flex; + justify-content: space-between; + align-items: center; + + .searchable-item-list { + &__item--add-token { + display: flex; + } + } + } + + &__input { + div { + border: 0; + } + } + + &__two-line-input { + input { + padding-bottom: 0; + } + } + } +} diff --git a/ui/pages/swaps/selected-token/selected-token.js b/ui/pages/swaps/selected-token/selected-token.js index 516dbdc246e3..174a5925282b 100644 --- a/ui/pages/swaps/selected-token/selected-token.js +++ b/ui/pages/swaps/selected-token/selected-token.js @@ -29,52 +29,54 @@ export default function SelectedToken({ }; return ( - <div - className={classnames( - 'dropdown-search-list', - 'dropdown-search-list__selector-closed-container', - 'dropdown-input-pair__selector--closed', - )} - data-testid="dropdown-search-list" - tabIndex="0" - onClick={onClick} - onKeyUp={onKeyUp} - > - <div className="dropdown-search-list__selector-closed"> - {hasIcon && ( - <UrlIcon - url={selectedToken.iconUrl} - className="dropdown-search-list__selector-closed-icon" - name={selectedToken?.symbol} - /> + <div className="selected-token"> + <div + className={classnames( + 'selected-token-list', + 'selected-token-list__selector-closed-container', + 'selected-token-input-pair__selector--closed', )} - <div - className={classnames('dropdown-search-list__labels', { - 'dropdown-search-list__labels--with-icon': hasIcon, - })} - > - <div className="dropdown-search-list__item-labels"> - <span - data-testid={testId} - className={classnames( - 'dropdown-search-list__closed-primary-label', - { - 'dropdown-search-list__select-default': - !selectedToken?.symbol, - }, - )} - > - {selectedToken?.symbol || t('swapSelectAToken')} - </span> + data-testid="selected-token-list" + tabIndex="0" + onClick={onClick} + onKeyUp={onKeyUp} + > + <div className="selected-token-list__selector-closed"> + {hasIcon && ( + <UrlIcon + url={selectedToken.iconUrl} + className="selected-token-list__selector-closed-icon" + name={selectedToken?.symbol} + /> + )} + <div + className={classnames('selected-token-list__labels', { + 'selected-token-list__labels--with-icon': hasIcon, + })} + > + <div className="selected-token-list__item-labels"> + <span + data-testid={testId} + className={classnames( + 'selected-token-list__closed-primary-label', + { + 'selected-token-list__select-default': + !selectedToken?.symbol, + }, + )} + > + {selectedToken?.symbol || t('swapSelectAToken')} + </span> + </div> </div> </div> + <Icon + name={IconName.ArrowDown} + size={IconSize.Xs} + marginRight={3} + color={IconColor.iconAlternative} + /> </div> - <Icon - name={IconName.ArrowDown} - size={IconSize.Xs} - marginRight={3} - color={IconColor.iconAlternative} - /> </div> ); } diff --git a/ui/pages/swaps/selected-token/selected-token.test.js b/ui/pages/swaps/selected-token/selected-token.test.js index 15cca222a8ae..af70ed0bd8f1 100644 --- a/ui/pages/swaps/selected-token/selected-token.test.js +++ b/ui/pages/swaps/selected-token/selected-token.test.js @@ -37,7 +37,7 @@ describe('SelectedToken', () => { it('renders the component and opens the list', () => { const props = createProps(); const { getByTestId } = renderWithProvider(<SelectedToken {...props} />); - const dropdownSearchList = getByTestId('dropdown-search-list'); + const dropdownSearchList = getByTestId('selected-token-list'); expect(dropdownSearchList).toBeInTheDocument(); fireEvent.click(dropdownSearchList); expect(props.onClick).toHaveBeenCalledTimes(1); diff --git a/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap b/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap deleted file mode 100644 index 4f1f1d15f355..000000000000 --- a/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SlippageButtons renders the component with initial props 1`] = ` -<button - class="slippage-buttons__header slippage-buttons__header--open" -> - <span - class="mm-box mm-text mm-text--body-sm-bold mm-box--margin-right-2 mm-box--color-primary-default" - > - Advanced options - </span> - <i - class="fa fa-angle-up" - /> -</button> -`; - -exports[`SlippageButtons renders the component with initial props 2`] = ` -<div - class="button-group slippage-buttons__button-group radio-button-group" - role="radiogroup" -> - <button - aria-checked="false" - class="button-group__button radio-button" - data-testid="button-group__button0" - role="radio" - > - 2% - </button> - <button - aria-checked="true" - class="button-group__button radio-button button-group__button--active radio-button--active" - data-testid="button-group__button1" - role="radio" - > - 3% - </button> - <button - aria-checked="false" - class="button-group__button slippage-buttons__button-group-custom-button radio-button" - data-testid="button-group__button2" - role="radio" - > - custom - </button> -</div> -`; diff --git a/ui/pages/swaps/slippage-buttons/index.js b/ui/pages/swaps/slippage-buttons/index.js deleted file mode 100644 index 6cdbe8843019..000000000000 --- a/ui/pages/swaps/slippage-buttons/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './slippage-buttons'; diff --git a/ui/pages/swaps/slippage-buttons/index.scss b/ui/pages/swaps/slippage-buttons/index.scss deleted file mode 100644 index de6e68ba3556..000000000000 --- a/ui/pages/swaps/slippage-buttons/index.scss +++ /dev/null @@ -1,111 +0,0 @@ -@use "design-system"; - -.slippage-buttons { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - - &__header { - display: flex; - align-items: center; - color: var(--color-primary-default); - margin-bottom: 0; - margin-left: auto; - margin-right: auto; - background: unset; - - &--open { - margin-bottom: 8px; - } - } - - &__content { - padding-left: 10px; - } - - &__dropdown-content { - display: flex; - align-items: center; - } - - &__buttons-prefix { - display: flex; - align-items: center; - margin-right: 8px; - } - - &__button-group { - & &-custom-button { - cursor: text; - display: flex; - align-items: center; - justify-content: center; - position: relative; - min-width: 72px; - margin-right: 0; - } - } - - &__custom-input { - display: flex; - justify-content: center; - - input { - border: none; - width: 64px; - text-align: center; - background: var(--color-primary-default); - color: var(--color-primary-inverse); - font-weight: inherit; - - &::-webkit-input-placeholder { /* WebKit, Blink, Edge */ - color: var(--color-primary-inverse); - } - - &:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ - color: var(--color-primary-inverse); - opacity: 1; - } - - &::-moz-placeholder { /* Mozilla Firefox 19+ */ - color: var(--color-primary-inverse); - opacity: 1; - } - - &:-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: var(--color-primary-inverse); - } - - &::-ms-input-placeholder { /* Microsoft Edge */ - color: var(--color-primary-inverse); - } - - &::placeholder { /* Most modern browsers support this now. */ - color: var(--color-primary-inverse); - } - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ - } - - input[type=number] { - -moz-appearance: textfield; - } - - &--danger { - input { - background: var(--color-error-default); - } - } - } - - &__percentage-suffix { - position: absolute; - right: 5px; - } -} diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.js deleted file mode 100644 index 387958753e3c..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.js +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { I18nContext } from '../../../contexts/i18n'; -import ButtonGroup from '../../../components/ui/button-group'; -import Button from '../../../components/ui/button'; -import InfoTooltip from '../../../components/ui/info-tooltip'; -import { Slippage } from '../../../../shared/constants/swaps'; -import { Text } from '../../../components/component-library'; -import { - TextVariant, - TextColor, -} from '../../../helpers/constants/design-system'; - -export default function SlippageButtons({ - onSelect, - maxAllowedSlippage, - currentSlippage, - isDirectWrappingEnabled, -}) { - const t = useContext(I18nContext); - const [customValue, setCustomValue] = useState(() => { - if ( - typeof currentSlippage === 'number' && - !Object.values(Slippage).includes(currentSlippage) - ) { - return currentSlippage.toString(); - } - return ''; - }); - const [enteringCustomValue, setEnteringCustomValue] = useState(false); - const [activeButtonIndex, setActiveButtonIndex] = useState(() => { - if (currentSlippage === Slippage.high) { - return 1; // 3% slippage. - } else if (currentSlippage === Slippage.default) { - return 0; // 2% slippage. - } else if (typeof currentSlippage === 'number') { - return 2; // Custom slippage. - } - return 0; - }); - const [open, setOpen] = useState(() => { - return currentSlippage !== Slippage.default; // Only open Advanced options by default if it's not default slippage. - }); - const [inputRef, setInputRef] = useState(null); - - let errorText = ''; - if (customValue) { - // customValue is a string, e.g. '0' - if (Number(customValue) < 0) { - errorText = t('swapSlippageNegative'); - } else if (Number(customValue) > 0 && Number(customValue) <= 1) { - // We will not show this warning for 0% slippage, because we will only - // return non-slippage quotes from off-chain makers. - errorText = t('swapLowSlippageError'); - } else if ( - Number(customValue) >= 5 && - Number(customValue) <= maxAllowedSlippage - ) { - errorText = t('swapHighSlippageWarning'); - } else if (Number(customValue) > maxAllowedSlippage) { - errorText = t('swapsExcessiveSlippageWarning'); - } - } - - const customValueText = customValue || t('swapCustom'); - - useEffect(() => { - if ( - inputRef && - enteringCustomValue && - window.document.activeElement !== inputRef - ) { - inputRef.focus(); - } - }, [inputRef, enteringCustomValue]); - - return ( - <div className="slippage-buttons"> - <button - onClick={() => setOpen(!open)} - className={classnames('slippage-buttons__header', { - 'slippage-buttons__header--open': open, - })} - > - <Text - variant={TextVariant.bodySmBold} - marginRight={2} - color={TextColor.primaryDefault} - as="span" - > - {t('swapsAdvancedOptions')} - </Text> - {open ? ( - <i className="fa fa-angle-up" /> - ) : ( - <i className="fa fa-angle-down" /> - )} - </button> - <div className="slippage-buttons__content"> - {open && ( - <> - {!isDirectWrappingEnabled && ( - <div className="slippage-buttons__dropdown-content"> - <div className="slippage-buttons__buttons-prefix"> - <Text - variant={TextVariant.bodySmBold} - marginRight={1} - color={TextColor.textDefault} - > - {t('swapsMaxSlippage')} - </Text> - <InfoTooltip - position="top" - contentText={t('swapSlippageTooltip')} - /> - </div> - <ButtonGroup - defaultActiveButtonIndex={ - activeButtonIndex === 2 && !customValue - ? 1 - : activeButtonIndex - } - variant="radiogroup" - newActiveButtonIndex={activeButtonIndex} - className={classnames( - 'button-group', - 'slippage-buttons__button-group', - )} - > - <Button - onClick={() => { - setCustomValue(''); - setEnteringCustomValue(false); - setActiveButtonIndex(0); - onSelect(Slippage.default); - }} - > - {t('swapSlippagePercent', [Slippage.default])} - </Button> - <Button - onClick={() => { - setCustomValue(''); - setEnteringCustomValue(false); - setActiveButtonIndex(1); - onSelect(Slippage.high); - }} - > - {t('swapSlippagePercent', [Slippage.high])} - </Button> - <Button - className={classnames( - 'slippage-buttons__button-group-custom-button', - { - 'radio-button--danger': errorText, - }, - )} - onClick={() => { - setActiveButtonIndex(2); - setEnteringCustomValue(true); - }} - > - {enteringCustomValue ? ( - <div - className={classnames( - 'slippage-buttons__custom-input', - { - 'slippage-buttons__custom-input--danger': errorText, - }, - )} - > - <input - data-testid="slippage-buttons__custom-slippage" - onChange={(event) => { - const { value } = event.target; - const isValueNumeric = !isNaN(Number(value)); - if (isValueNumeric) { - setCustomValue(value); - onSelect(Number(value)); - } - }} - type="text" - maxLength="4" - ref={setInputRef} - onBlur={() => { - setEnteringCustomValue(false); - }} - value={customValue || ''} - /> - </div> - ) : ( - customValueText - )} - {(customValue || enteringCustomValue) && ( - <div className="slippage-buttons__percentage-suffix"> - % - </div> - )} - </Button> - </ButtonGroup> - </div> - )} - </> - )} - {errorText && ( - <Text - variant={TextVariant.bodyXs} - color={TextColor.errorDefault} - marginTop={2} - > - {errorText} - </Text> - )} - </div> - </div> - ); -} - -SlippageButtons.propTypes = { - onSelect: PropTypes.func.isRequired, - maxAllowedSlippage: PropTypes.number.isRequired, - currentSlippage: PropTypes.number, - isDirectWrappingEnabled: PropTypes.bool, -}; diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js deleted file mode 100644 index 68cfd8762f44..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import SlippageButtons from './slippage-buttons'; - -export default { - title: 'Pages/Swaps/SlippageButtons', -}; - -export const DefaultStory = () => ( - <div style={{ height: '200px', marginTop: '160px' }}> - <SlippageButtons onSelect={action('slippage')} /> - </div> -); - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js deleted file mode 100644 index 7834eefd59ea..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; - -import { renderWithProvider, fireEvent } from '../../../../test/jest'; -import { Slippage } from '../../../../shared/constants/swaps'; -import SlippageButtons from './slippage-buttons'; - -const createProps = (customProps = {}) => { - return { - onSelect: jest.fn(), - maxAllowedSlippage: 15, - currentSlippage: Slippage.high, - smartTransactionsEnabled: false, - ...customProps, - }; -}; - -describe('SlippageButtons', () => { - it('renders the component with initial props', () => { - const { getByText, queryByText, getByTestId } = renderWithProvider( - <SlippageButtons {...createProps()} />, - ); - expect(getByText('2%')).toBeInTheDocument(); - expect(getByText('3%')).toBeInTheDocument(); - expect(getByText('custom')).toBeInTheDocument(); - expect(getByText('Advanced options')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__header'), - ).toMatchSnapshot(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); - expect(queryByText('Smart Swaps')).not.toBeInTheDocument(); - expect(getByTestId('button-group__button1')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('renders slippage with a custom value', () => { - const { getByText } = renderWithProvider( - <SlippageButtons {...createProps({ currentSlippage: 2.5 })} />, - ); - expect(getByText('2.5')).toBeInTheDocument(); - }); - - it('renders the default slippage with Advanced options hidden', () => { - const { getByText, queryByText } = renderWithProvider( - <SlippageButtons - {...createProps({ currentSlippage: Slippage.default })} - />, - ); - expect(getByText('Advanced options')).toBeInTheDocument(); - expect(document.querySelector('.fa-angle-down')).toBeInTheDocument(); - expect(queryByText('2%')).not.toBeInTheDocument(); - }); - - it('opens the Advanced options section and sets a default slippage', () => { - const { getByText, getByTestId } = renderWithProvider( - <SlippageButtons - {...createProps({ currentSlippage: Slippage.default })} - />, - ); - fireEvent.click(getByText('Advanced options')); - fireEvent.click(getByTestId('button-group__button0')); - expect(getByTestId('button-group__button0')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('opens the Advanced options section and sets a high slippage', () => { - const { getByText, getByTestId } = renderWithProvider( - <SlippageButtons - {...createProps({ currentSlippage: Slippage.default })} - />, - ); - fireEvent.click(getByText('Advanced options')); - fireEvent.click(getByTestId('button-group__button1')); - expect(getByTestId('button-group__button1')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('sets a custom slippage value', () => { - const { getByTestId } = renderWithProvider( - <SlippageButtons {...createProps()} />, - ); - fireEvent.click(getByTestId('button-group__button2')); - expect(getByTestId('button-group__button2')).toHaveAttribute( - 'aria-checked', - 'true', - ); - const input = getByTestId('slippage-buttons__custom-slippage'); - fireEvent.change(input, { target: { value: 5 } }); - fireEvent.click(document); - expect(input).toHaveAttribute('value', '5'); - }); -}); diff --git a/ui/pages/swaps/smart-transaction-status/index.scss b/ui/pages/swaps/smart-transaction-status/index.scss index 4229acca71c0..f21e3e7752bf 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; } } @@ -83,4 +70,16 @@ &__remaining-time { font-variant-numeric: tabular-nums; } + + &__icon, + &__icon-fallback { + height: 16px; + width: 16px; + } + + &__icon-fallback { + padding-top: 0; + font-size: 12px; + line-height: 16px; + } } diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index 157190687f31..7b8d5910c218 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, @@ -21,13 +20,13 @@ import { getRpcPrefsForCurrentProvider, } from '../../../selectors'; import { - getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, + getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; import { DEFAULT_ROUTE, - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import { Text } from '../../../components/component-library'; import Box from '../../../components/ui/box'; @@ -74,16 +73,11 @@ 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, ); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); const chainId = useSelector(getCurrentChainId); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); @@ -131,7 +125,7 @@ export default function SmartTransactionStatusPage() { hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, + stx_user_opt_in: useSelector(getSmartTransactionsOptInStatusForMetrics), }; let destinationValue; @@ -326,12 +320,12 @@ export default function SmartTransactionStatusPage() { {fetchParamsSourceTokenInfo.iconUrl ? ( <UrlIcon url={fetchParamsSourceTokenInfo.iconUrl} - className="main-quote-summary__icon" + className="smart-transactions-status-summary__icon" name={ fetchParamsSourceTokenInfo.symbol ?? latestSmartTransaction?.destinationTokenSymbol } - fallbackClassName="main-quote-summary__icon-fallback" + fallbackClassName="smart-transactions-status-summary__icon-fallback" /> ) : null} <Box display={DISPLAY.BLOCK} marginLeft={2} marginRight={2}> @@ -340,12 +334,12 @@ export default function SmartTransactionStatusPage() { {fetchParamsDestinationTokenInfo.iconUrl ? ( <UrlIcon url={fetchParamsDestinationTokenInfo.iconUrl} - className="main-quote-summary__icon" + className="smart-transactions-status-summary__icon" name={ fetchParamsDestinationTokenInfo.symbol ?? latestSmartTransaction?.destinationTokenSymbol } - fallbackClassName="main-quote-summary__icon-fallback" + fallbackClassName="smart-transactions-status-summary__icon-fallback" /> ) : null} <Text @@ -368,7 +362,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}> @@ -404,6 +398,7 @@ export default function SmartTransactionStatusPage() { </Box> )} <Text + data-testid="swap-smart-transaction-status-header" color={TextColor.textDefault} variant={TextVariant.headingSm} as="h4" @@ -427,6 +422,7 @@ export default function SmartTransactionStatusPage() { )} {description && ( <Text + data-testid="swap-smart-transaction-status-description" variant={TextVariant.bodySm} as="h6" marginTop={blockExplorerUrl && 1} @@ -443,7 +439,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 @@ -468,7 +464,7 @@ export default function SmartTransactionStatusPage() { await dispatch(prepareToLeaveSwaps()); history.push(DEFAULT_ROUTE); } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } }} onCancel={async () => { 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/__snapshots__/view-quote-price-difference.test.js.snap b/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap deleted file mode 100644 index 5116743379bc..000000000000 --- a/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap +++ /dev/null @@ -1,233 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`View Price Quote Difference displays a fiat error when calculationError is present 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper high" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Check your rate before proceeding - </div> - <div> - <div - aria-describedby="tippy-tooltip-4" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping. - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`View Price Quote Difference displays an error when in high bucket 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper high" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Price difference of ~% - </div> - <div> - <div - aria-describedby="tippy-tooltip-3" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - You are about to swap 1 ETH (~) for 42.947749 LINK (~). - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`View Price Quote Difference displays an error when in medium bucket 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper medium" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Price difference of ~% - </div> - <div> - <div - aria-describedby="tippy-tooltip-2" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - You are about to swap 1 ETH (~) for 42.947749 LINK (~). - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`View Price Quote Difference should match snapshot 1`] = ` -<div> - <div - class="view-quote__price-difference-warning-wrapper low" - > - <div - class="actionable-message" - > - - <div - class="actionable-message__message" - > - <div - class="view-quote__price-difference-warning-contents" - > - <div - class="view-quote__price-difference-warning-contents-text" - > - <div - class="box box--padding-bottom-2 box--display-flex box--flex-direction-row box--justify-content-space-between" - > - <div - class="view-quote__price-difference-warning-contents-title" - > - Price difference of ~% - </div> - <div> - <div - aria-describedby="tippy-tooltip-1" - class="" - data-original-title="Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - data-tooltipped="" - style="display: inline;" - tabindex="0" - > - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit" - style="mask-image: url('./images/icons/info.svg');" - /> - </div> - </div> - </div> - You are about to swap 1 ETH (~) for 42.947749 LINK (~). - <div - class="view-quote__price-difference-warning-contents-actions" - > - <button> - I understand - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</div> -`; diff --git a/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap b/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap deleted file mode 100644 index 3f6273f9f738..000000000000 --- a/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ViewQuote renders the component with EIP-1559 enabled 1`] = ` -<div - class="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" -> - <span - class="main-quote-summary__source-row-value" - title="10" - > - 10 - </span> - <div - class="" - > - <img - alt="DAI" - class="url-icon main-quote-summary__icon" - src="https://foo.bar/logo.png" - /> - </div> - <span - class="main-quote-summary__source-row-symbol" - title="DAI" - > - DAI - </span> -</div> -`; - -exports[`ViewQuote renders the component with EIP-1559 enabled 2`] = ` -<div - class="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" -> - <div - class="exchange-rate-display main-quote-summary__exchange-rate-display" - > - <div - class="box exchange-rate-display__quote-rate--no-link box--display-flex box--gap-1 box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default" - data-testid="exchange-rate-display-quote-rate" - > - <span> - 1 - </span> - <span - class="" - data-testid="exchange-rate-display-base-symbol" - > - DAI - </span> - <span> - = - </span> - <span> - 2.2 - </span> - <span - class="" - > - USDC - </span> - </div> - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative" - data-testid="exchange-rate-display-switch" - style="mask-image: url('./images/icons/swap-horizontal.svg'); cursor: pointer;" - title="Switch" - /> - </div> -</div> -`; - -exports[`ViewQuote renders the component with initial props 1`] = ` -<div - class="main-quote-summary__source-row" - data-testid="main-quote-summary__source-row" -> - <span - class="main-quote-summary__source-row-value" - title="10" - > - 10 - </span> - <div - class="" - > - <img - alt="DAI" - class="url-icon main-quote-summary__icon" - src="https://foo.bar/logo.png" - /> - </div> - <span - class="main-quote-summary__source-row-symbol" - title="DAI" - > - DAI - </span> -</div> -`; - -exports[`ViewQuote renders the component with initial props 2`] = ` -<div - class="main-quote-summary__exchange-rate-container" - data-testid="main-quote-summary__exchange-rate-container" -> - <div - class="exchange-rate-display main-quote-summary__exchange-rate-display" - > - <div - class="box exchange-rate-display__quote-rate--no-link box--display-flex box--gap-1 box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default" - data-testid="exchange-rate-display-quote-rate" - > - <span> - 1 - </span> - <span - class="" - data-testid="exchange-rate-display-base-symbol" - > - DAI - </span> - <span> - = - </span> - <span> - 2.2 - </span> - <span - class="" - > - USDC - </span> - </div> - <span - class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative" - data-testid="exchange-rate-display-switch" - style="mask-image: url('./images/icons/swap-horizontal.svg'); cursor: pointer;" - title="Switch" - /> - </div> -</div> -`; diff --git a/ui/pages/swaps/view-quote/index.js b/ui/pages/swaps/view-quote/index.js deleted file mode 100644 index 4a412aa905fe..000000000000 --- a/ui/pages/swaps/view-quote/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './view-quote'; diff --git a/ui/pages/swaps/view-quote/index.scss b/ui/pages/swaps/view-quote/index.scss deleted file mode 100644 index f96451cc6f4c..000000000000 --- a/ui/pages/swaps/view-quote/index.scss +++ /dev/null @@ -1,179 +0,0 @@ -@use "design-system"; - -.view-quote { - display: flex; - flex-flow: column; - align-items: center; - flex: 1; - width: 100%; - - &::after { // Hide preloaded images. - position: absolute; - width: 0; - height: 0; - overflow: hidden; - z-index: -1; - content: url('/images/transaction-background-top.svg') url('/images/transaction-background-bottom.svg'); // Preload images for the STX status page. - } - - &__content { - display: flex; - flex-flow: column; - align-items: center; - width: 100%; - height: 100%; - padding-left: 20px; - padding-right: 20px; - - &_modal > div:not(.view-quote__warning-wrapper) { - opacity: 0.6; - pointer-events: none; - } - - @include design-system.screen-sm-max { - overflow-y: auto; - max-height: 420px; - } - } - - @include design-system.screen-sm-min { - width: 348px; - } - - &__price-difference-warning { - &-wrapper { - width: 100%; - - &.low, - &.medium, - &.high { - .actionable-message { - &::before { - background: none; - } - - .actionable-message__message { - color: inherit; - } - - button { - font-size: design-system.$font-size-h8; - padding: 4px 12px; - border-radius: 42px; - } - } - } - - &.low { - .actionable-message { - button { - background: var(--color-primary-default); - color: var(--color-primary-inverse); - } - } - } - - &.medium { - .actionable-message { - border-color: var(--color-warning-default); - background: var(--color-warning-muted); - - button { - background: var(--color-warning-default); - } - } - } - - &.high { - .actionable-message { - border-color: var(--color-error-default); - background: var(--color-error-muted); - - button { - background: var(--color-error-default); - color: var(--color-error-inverse); - } - } - } - } - - &-contents { - display: flex; - text-align: left; - - &-title { - font-weight: bold; - } - - &-actions { - text-align: end; - padding-top: 10px; - } - - i { - margin-inline-start: 10px; - } - } - } - - &__warning-wrapper { - width: 100%; - align-items: center; - justify-content: center; - max-width: 340px; - margin-top: 8px; - margin-bottom: 8px; - - @include design-system.screen-sm-min { - &--thin { - min-height: 36px; - } - - display: flex; - } - } - - &__bold { - font-weight: bold; - } - - &__countdown-timer-container { - display: flex; - justify-content: center; - margin-top: 8px; - } - - &__fee-card-container { - display: flex; - align-items: center; - width: 100%; - max-width: 311px; - margin-bottom: 8px; - - @include design-system.screen-sm-min { - margin-bottom: 0; - } - } - - &__metamask-rate { - display: flex; - } - - &__metamask-rate-text { - @include design-system.H7; - - color: var(--color-text-alternative); - } - - &__metamask-rate-info-icon { - margin-left: 4px; - } - - &__thin-swaps-footer { - max-height: 82px; - - @include design-system.screen-sm-min { - height: 72px; - } - } -} diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.js b/ui/pages/swaps/view-quote/view-quote-price-difference.js deleted file mode 100644 index 2607243494d4..000000000000 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useContext } from 'react'; - -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { I18nContext } from '../../../contexts/i18n'; - -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import Tooltip from '../../../components/ui/tooltip'; -import Box from '../../../components/ui/box'; -import { - JustifyContent, - DISPLAY, -} from '../../../helpers/constants/design-system'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import { Icon, IconName } from '../../../components/component-library'; - -export default function ViewQuotePriceDifference(props) { - const { - usedQuote, - sourceTokenValue, - destinationTokenValue, - onAcknowledgementClick, - acknowledged, - priceSlippageFromSource, - priceSlippageFromDestination, - priceDifferencePercentage, - priceSlippageUnknownFiatValue, - } = props; - - const t = useContext(I18nContext); - - let priceDifferenceTitle = ''; - let priceDifferenceMessage = ''; - let priceDifferenceClass = ''; - let priceDifferenceAcknowledgementText = ''; - if (priceSlippageUnknownFiatValue) { - // A calculation error signals we cannot determine dollar value - priceDifferenceTitle = t('swapPriceUnavailableTitle'); - priceDifferenceMessage = t('swapPriceUnavailableDescription'); - priceDifferenceClass = GasRecommendations.high; - priceDifferenceAcknowledgementText = t('tooltipApproveButton'); - } else { - priceDifferenceTitle = t('swapPriceDifferenceTitle', [ - priceDifferencePercentage, - ]); - priceDifferenceMessage = t('swapPriceDifference', [ - sourceTokenValue, // Number of source token to swap - usedQuote.sourceTokenInfo.symbol, // Source token symbol - priceSlippageFromSource, // Source tokens total value - destinationTokenValue, // Number of destination tokens in return - usedQuote.destinationTokenInfo.symbol, // Destination token symbol, - priceSlippageFromDestination, // Destination tokens total value - ]); - priceDifferenceClass = usedQuote.priceSlippage.bucket; - priceDifferenceAcknowledgementText = t('tooltipApproveButton'); - } - - return ( - <div - className={classnames( - 'view-quote__price-difference-warning-wrapper', - priceDifferenceClass, - )} - > - <ActionableMessage - message={ - <div className="view-quote__price-difference-warning-contents"> - <div className="view-quote__price-difference-warning-contents-text"> - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.spaceBetween} - paddingBottom={2} - > - <div className="view-quote__price-difference-warning-contents-title"> - {priceDifferenceTitle} - </div> - <Tooltip position="bottom" title={t('swapPriceImpactTooltip')}> - <Icon name={IconName.Info} /> - </Tooltip> - </Box> - {priceDifferenceMessage} - {!acknowledged && ( - <div className="view-quote__price-difference-warning-contents-actions"> - <button - onClick={() => { - onAcknowledgementClick(); - }} - > - {priceDifferenceAcknowledgementText} - </button> - </div> - )} - </div> - </div> - } - /> - </div> - ); -} - -ViewQuotePriceDifference.propTypes = { - usedQuote: PropTypes.object, - sourceTokenValue: PropTypes.string, - destinationTokenValue: PropTypes.string, - onAcknowledgementClick: PropTypes.func, - acknowledged: PropTypes.bool, - priceSlippageFromSource: PropTypes.string, - priceSlippageFromDestination: PropTypes.string, - priceDifferencePercentage: PropTypes.number, - priceSlippageUnknownFiatValue: PropTypes.bool, -}; diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js deleted file mode 100644 index 5702f39f67fe..000000000000 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../test/lib/render-helpers'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import ViewQuotePriceDifference from './view-quote-price-difference'; - -describe('View Price Quote Difference', () => { - const mockState = { - metamask: { - tokens: [], - preferences: { showFiatInTestnets: true }, - currentCurrency: 'usd', - currencyRates: { - ETH: { - conversionRate: 600.0, - }, - }, - }, - }; - - const mockStore = configureMockStore()(mockState); - - // Sample transaction is 1 $ETH to ~42.880915 $LINK - const DEFAULT_PROPS = { - usedQuote: { - trade: { - data: '0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000007756e69737761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca0000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000024855454cb32d335f0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000005fc7b7100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f161421c8e0000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca', - from: '0xd7440fdcb70a9fba55dfe06942ddbc17679c90ac', - value: '0xde0b6b3a7640000', - gas: '0xbbfd0', - to: '0x881D40237659C251811CEC9c364ef91dC08D300C', - }, - sourceAmount: '1000000000000000000', - destinationAmount: '42947749216634160067', - error: null, - sourceToken: '0x0000000000000000000000000000000000000000', - destinationToken: '0x514910771af9ca656af840dff83e8264ecf986ca', - approvalNeeded: null, - maxGas: 770000, - averageGas: 210546, - estimatedRefund: 80000, - fetchTime: 647, - aggregator: 'uniswap', - aggType: 'DEX', - fee: 0.875, - gasMultiplier: 1.5, - priceSlippage: { - ratio: 1.007876641534847, - calculationError: '', - bucket: GasRecommendations.low, - sourceAmountInETH: 1, - destinationAmountInETH: 0.9921849150875727, - }, - slippage: 2, - sourceTokenInfo: { - symbol: 'ETH', - name: 'Ether', - address: '0x0000000000000000000000000000000000000000', - decimals: 18, - iconUrl: 'images/black-eth-logo.png', - }, - destinationTokenInfo: { - address: '0x514910771af9ca656af840dff83e8264ecf986ca', - symbol: 'LINK', - decimals: 18, - occurances: 12, - iconUrl: - 'https://cloudflare-ipfs.com/ipfs/QmQhZAdcZvW9T2tPm516yHqbGkfhyZwTZmLixW9MXJudTA', - }, - ethFee: '0.011791', - ethValueOfTokens: '0.99220724791716534441', - overallValueOfQuote: '0.98041624791716534441', - metaMaskFeeInEth: '0.00875844985551091729', - isBestQuote: true, - savings: { - performance: '0.00207907025112527799', - fee: '0.005581', - metaMaskFee: '0.00875844985551091729', - total: '-0.0010983796043856393', - medianMetaMaskFee: '0.00874009740688812165', - }, - }, - sourceTokenValue: '1', - destinationTokenValue: '42.947749', - }; - - it('should match snapshot', () => { - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...DEFAULT_PROPS} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays an error when in medium bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GasRecommendations.medium; - - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...props} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays an error when in high bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GasRecommendations.high; - - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...props} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays a fiat error when calculationError is present', () => { - const props = { ...DEFAULT_PROPS, priceSlippageUnknownFiatValue: true }; - props.usedQuote.priceSlippage.calculationError = - 'Could not determine price.'; - - const { container } = renderWithProvider( - <ViewQuotePriceDifference {...props} />, - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js deleted file mode 100644 index 8dc17ac3c765..000000000000 --- a/ui/pages/swaps/view-quote/view-quote.js +++ /dev/null @@ -1,1090 +0,0 @@ -import React, { - useState, - useContext, - useMemo, - useEffect, - useRef, - useCallback, -} from 'react'; -import { shallowEqual, useSelector, useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import BigNumber from 'bignumber.js'; -import { isEqual } from 'lodash'; -import classnames from 'classnames'; -import { captureException } from '@sentry/browser'; - -import { I18nContext } from '../../../contexts/i18n'; -import SelectQuotePopover from '../select-quote-popover'; -import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; -import { usePrevious } from '../../../hooks/usePrevious'; -import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import FeeCard from '../fee-card'; -import { - getQuotes, - getSelectedQuote, - getApproveTxParams, - getFetchParams, - setBalanceError, - getQuotesLastFetched, - getBalanceError, - getCustomSwapsGas, // Gas limit. - getCustomMaxFeePerGas, - getCustomMaxPriorityFeePerGas, - getSwapsUserFeeLevel, - getDestinationTokenInfo, - getUsedSwapsGasPrice, - getTopQuote, - signAndSendTransactions, - getBackgroundSwapRouteState, - swapsQuoteSelected, - getSwapsQuoteRefreshTime, - getReviewSwapClickedTimestamp, - signAndSendSwapsSmartTransaction, - getSwapsNetworkConfig, - getSmartTransactionsError, - getCurrentSmartTransactionsError, - getSwapsSTXLoading, - fetchSwapsSmartTransactionFees, - getSmartTransactionFees, - getCurrentSmartTransactionsEnabled, -} from '../../../ducks/swaps/swaps'; -import { - conversionRateSelector, - getSelectedAccount, - getCurrentCurrency, - getTokenExchangeRates, - getSwapsDefaultToken, - getCurrentChainId, - isHardwareWallet, - getHardwareWalletType, - checkNetworkAndAccountSupports1559, - getUSDConversionRate, -} from '../../../selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; -import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; -import { - getLayer1GasFee, - safeRefetchQuotes, - setCustomApproveTxData, - setSwapsErrorKey, - showModal, - setSwapsQuotesPollingLimitEnabled, -} from '../../../store/actions'; -import { SET_SMART_TRANSACTIONS_ERROR } from '../../../store/actionConstants'; -import { - ASSET_ROUTE, - BUILD_QUOTE_ROUTE, - DEFAULT_ROUTE, - SWAPS_ERROR_ROUTE, - AWAITING_SWAP_ROUTE, -} from '../../../helpers/constants/routes'; -import MainQuoteSummary from '../main-quote-summary'; -import { getCustomTxParamsData } from '../../confirmations/confirm-approve/confirm-approve.util'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import { - quotesToRenderableData, - getRenderableNetworkFeesForQuote, - getFeeForSmartTransaction, -} from '../swaps.util'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { QUOTES_EXPIRED_ERROR } from '../../../../shared/constants/swaps'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import CountdownTimer from '../countdown-timer'; -import SwapsFooter from '../swaps-footer'; -import PulseLoader from '../../../components/ui/pulse-loader'; // TODO: Replace this with a different loading component. -import Box from '../../../components/ui/box'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { parseStandardTokenTransactionData } from '../../../../shared/modules/transaction.utils'; -import { getTokenValueParam } from '../../../../shared/lib/metamask-controller-utils'; -import { - calcGasTotal, - calcTokenAmount, - toPrecisionWithoutTrailingZeros, -} from '../../../../shared/lib/transactions-controller-utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { addHexPrefix } from '../../../../app/scripts/lib/util'; -import { - calcTokenValue, - calculateMaxGasLimit, -} from '../../../../shared/lib/swaps-utils'; -import { - addHexes, - decGWEIToHexWEI, - decimalToHex, - decWEIToDecETH, - hexWEIToDecGWEI, - sumHexes, -} from '../../../../shared/modules/conversion.utils'; -import ViewQuotePriceDifference from './view-quote-price-difference'; - -let intervalId; - -export default function ViewQuote() { - const history = useHistory(); - const dispatch = useDispatch(); - const t = useContext(I18nContext); - const trackEvent = useContext(MetaMetricsContext); - - const [dispatchedSafeRefetch, setDispatchedSafeRefetch] = useState(false); - const [submitClicked, setSubmitClicked] = useState(false); - const [selectQuotePopoverShown, setSelectQuotePopoverShown] = useState(false); - const [warningHidden, setWarningHidden] = useState(false); - const [originalApproveAmount, setOriginalApproveAmount] = useState(null); - const [multiLayerL1FeeTotal, setMultiLayerL1FeeTotal] = useState(null); - const [multiLayerL1ApprovalFeeTotal, setMultiLayerL1ApprovalFeeTotal] = - useState(null); - // We need to have currentTimestamp in state, otherwise it would change with each rerender. - const [currentTimestamp] = useState(Date.now()); - - const [acknowledgedPriceDifference, setAcknowledgedPriceDifference] = - useState(false); - const priceDifferenceRiskyBuckets = [ - GasRecommendations.high, - GasRecommendations.medium, - ]; - - const routeState = useSelector(getBackgroundSwapRouteState); - const quotes = useSelector(getQuotes, isEqual); - useEffect(() => { - if (!Object.values(quotes).length) { - history.push(BUILD_QUOTE_ROUTE); - } else if (routeState === 'awaiting') { - history.push(AWAITING_SWAP_ROUTE); - } - }, [history, quotes, routeState]); - - const quotesLastFetched = useSelector(getQuotesLastFetched); - - // Select necessary data - const gasPrice = useSelector(getUsedSwapsGasPrice); - const customMaxGas = useSelector(getCustomSwapsGas); - const customMaxFeePerGas = useSelector(getCustomMaxFeePerGas); - const customMaxPriorityFeePerGas = useSelector(getCustomMaxPriorityFeePerGas); - const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); - const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); - const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); - const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); - const conversionRate = useSelector(conversionRateSelector); - const USDConversionRate = useSelector(getUSDConversionRate); - const currentCurrency = useSelector(getCurrentCurrency); - const swapsTokens = useSelector(getTokens, isEqual); - const networkAndAccountSupports1559 = useSelector( - checkNetworkAndAccountSupports1559, - ); - const balanceError = useSelector(getBalanceError); - const fetchParams = useSelector(getFetchParams, isEqual); - const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const selectedQuote = useSelector(getSelectedQuote, isEqual); - const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; - const tradeValue = usedQuote?.trade?.value ?? '0x0'; - const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); - const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); - const chainId = useSelector(getCurrentChainId); - const nativeCurrencySymbol = useSelector(getNativeCurrency); - const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const swapsSTXLoading = useSelector(getSwapsSTXLoading); - const currentSmartTransactionsError = useSelector( - getCurrentSmartTransactionsError, - ); - const smartTransactionsError = useSelector(getSmartTransactionsError); - const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); - const unsignedTransaction = usedQuote.trade; - const isSmartTransaction = - currentSmartTransactionsEnabled && smartTransactionsOptInStatus; - - let gasFeeInputs; - if (networkAndAccountSupports1559) { - // For Swaps we want to get 'high' estimations by default. - // eslint-disable-next-line react-hooks/rules-of-hooks - gasFeeInputs = useGasFeeInputs(GasRecommendations.high, { - userFeeLevel: swapsUserFeeLevel || GasRecommendations.high, - }); - } - - const fetchParamsSourceToken = fetchParams?.sourceToken; - - const additionalTrackingParams = { - reg_tx_fee_in_usd: undefined, - reg_tx_fee_in_eth: undefined, - reg_tx_max_fee_in_usd: undefined, - reg_tx_max_fee_in_eth: undefined, - stx_fee_in_usd: undefined, - stx_fee_in_eth: undefined, - stx_max_fee_in_usd: undefined, - stx_max_fee_in_eth: undefined, - }; - - const usedGasLimit = - usedQuote?.gasEstimateWithRefund || - `0x${decimalToHex(usedQuote?.averageGas || 0)}`; - - const maxGasLimit = calculateMaxGasLimit( - usedQuote?.gasEstimate, - usedQuote?.gasMultiplier, - usedQuote?.maxGas, - customMaxGas, - ); - - let maxFeePerGas; - let maxPriorityFeePerGas; - let baseAndPriorityFeePerGas; - - // EIP-1559 gas fees. - if (networkAndAccountSupports1559) { - const { - maxFeePerGas: suggestedMaxFeePerGas, - maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas, - gasFeeEstimates, - } = gasFeeInputs; - const estimatedBaseFee = gasFeeEstimates?.estimatedBaseFee ?? '0'; - maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); - maxPriorityFeePerGas = - customMaxPriorityFeePerGas || - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas); - baseAndPriorityFeePerGas = addHexes( - decGWEIToHexWEI(estimatedBaseFee), - maxPriorityFeePerGas, - ); - } - let gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); - if (multiLayerL1FeeTotal !== null) { - gasTotalInWeiHex = sumHexes( - gasTotalInWeiHex || '0x0', - multiLayerL1FeeTotal || '0x0', - ); - } - - const { tokensWithBalances } = useTokenTracker({ - tokens: swapsTokens, - includeFailedTokens: true, - }); - const balanceToken = - fetchParamsSourceToken === defaultSwapsToken.address - ? defaultSwapsToken - : tokensWithBalances.find(({ address }) => - isEqualCaseInsensitive(address, fetchParamsSourceToken), - ); - - const selectedFromToken = balanceToken || usedQuote.sourceTokenInfo; - const tokenBalance = - tokensWithBalances?.length && - calcTokenAmount( - selectedFromToken.balance || '0x0', - selectedFromToken.decimals, - ).toFixed(9); - const tokenBalanceUnavailable = - tokensWithBalances && balanceToken === undefined; - - const approveData = parseStandardTokenTransactionData(approveTxParams?.data); - const approveValue = approveData && getTokenValueParam(approveData); - const approveAmount = - approveValue && - selectedFromToken?.decimals !== undefined && - calcTokenAmount(approveValue, selectedFromToken.decimals).toFixed(9); - const approveGas = approveTxParams?.gas; - - const renderablePopoverData = useMemo(() => { - return quotesToRenderableData({ - quotes, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, - conversionRate, - currentCurrency, - approveGas, - tokenConversionRates: memoizedTokenConversionRates, - chainId, - smartTransactionEstimatedGas: - smartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees, - nativeCurrencySymbol, - multiLayerL1ApprovalFeeTotal, - }); - }, [ - quotes, - gasPrice, - baseAndPriorityFeePerGas, - networkAndAccountSupports1559, - conversionRate, - currentCurrency, - approveGas, - memoizedTokenConversionRates, - chainId, - smartTransactionFees?.tradeTxFees, - nativeCurrencySymbol, - smartTransactionsEnabled, - smartTransactionsOptInStatus, - multiLayerL1ApprovalFeeTotal, - ]); - - const renderableDataForUsedQuote = renderablePopoverData.find( - (renderablePopoverDatum) => - renderablePopoverDatum.aggId === usedQuote.aggregator, - ); - - const { - destinationTokenDecimals, - destinationTokenSymbol, - destinationTokenValue, - destinationIconUrl, - sourceTokenDecimals, - sourceTokenSymbol, - sourceTokenValue, - sourceTokenIconUrl, - } = renderableDataForUsedQuote; - - let { feeInFiat, feeInEth, rawEthFee, feeInUsd } = - getRenderableNetworkFeesForQuote({ - tradeGas: usedGasLimit, - approveGas, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, - currentCurrency, - conversionRate, - USDConversionRate, - tradeValue, - sourceSymbol: sourceTokenSymbol, - sourceAmount: usedQuote.sourceAmount, - chainId, - nativeCurrencySymbol, - multiLayerL1FeeTotal, - }); - additionalTrackingParams.reg_tx_fee_in_usd = Number(feeInUsd); - additionalTrackingParams.reg_tx_fee_in_eth = Number(rawEthFee); - - const renderableMaxFees = getRenderableNetworkFeesForQuote({ - tradeGas: maxGasLimit, - approveGas, - gasPrice: maxFeePerGas || gasPrice, - currentCurrency, - conversionRate, - USDConversionRate, - tradeValue, - sourceSymbol: sourceTokenSymbol, - sourceAmount: usedQuote.sourceAmount, - chainId, - nativeCurrencySymbol, - multiLayerL1FeeTotal, - }); - let { - feeInFiat: maxFeeInFiat, - feeInEth: maxFeeInEth, - rawEthFee: maxRawEthFee, - feeInUsd: maxFeeInUsd, - } = renderableMaxFees; - additionalTrackingParams.reg_tx_max_fee_in_usd = Number(maxFeeInUsd); - additionalTrackingParams.reg_tx_max_fee_in_eth = Number(maxRawEthFee); - - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees - ) { - const stxEstimatedFeeInWeiDec = - smartTransactionFees?.tradeTxFees.feeEstimate + - (smartTransactionFees?.approvalTxFees?.feeEstimate || 0); - const stxMaxFeeInWeiDec = - stxEstimatedFeeInWeiDec * swapsNetworkConfig.stxMaxFeeMultiplier; - ({ feeInFiat, feeInEth, rawEthFee, feeInUsd } = getFeeForSmartTransaction({ - chainId, - currentCurrency, - conversionRate, - USDConversionRate, - nativeCurrencySymbol, - feeInWeiDec: stxEstimatedFeeInWeiDec, - })); - additionalTrackingParams.stx_fee_in_usd = Number(feeInUsd); - additionalTrackingParams.stx_fee_in_eth = Number(rawEthFee); - additionalTrackingParams.estimated_gas = - smartTransactionFees?.tradeTxFees.gasLimit; - ({ - feeInFiat: maxFeeInFiat, - feeInEth: maxFeeInEth, - rawEthFee: maxRawEthFee, - feeInUsd: maxFeeInUsd, - } = getFeeForSmartTransaction({ - chainId, - currentCurrency, - conversionRate, - USDConversionRate, - nativeCurrencySymbol, - feeInWeiDec: stxMaxFeeInWeiDec, - })); - additionalTrackingParams.stx_max_fee_in_usd = Number(maxFeeInUsd); - additionalTrackingParams.stx_max_fee_in_eth = Number(maxRawEthFee); - } - - const tokenCost = new BigNumber(usedQuote.sourceAmount); - const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus( - new BigNumber(gasTotalInWeiHex, 16), - ); - - const insufficientTokens = - (tokensWithBalances?.length || balanceError) && - tokenCost.gt(new BigNumber(selectedFromToken.balance || '0x0')); - - const insufficientEth = ethCost.gt(new BigNumber(ethBalance || '0x0')); - - const tokenBalanceNeeded = insufficientTokens - ? toPrecisionWithoutTrailingZeros( - calcTokenAmount(tokenCost, selectedFromToken.decimals) - .minus(tokenBalance) - .toString(10), - 6, - ) - : null; - - const ethBalanceNeeded = insufficientEth - ? toPrecisionWithoutTrailingZeros( - ethCost - .minus(ethBalance, 16) - .div('1000000000000000000', 10) - .toString(10), - 6, - ) - : null; - - let ethBalanceNeededStx; - if (isSmartTransaction && smartTransactionsError?.balanceNeededWei) { - ethBalanceNeededStx = decWEIToDecETH( - smartTransactionsError.balanceNeededWei - - smartTransactionsError.currentBalanceWei, - ); - } - - const destinationToken = useSelector(getDestinationTokenInfo, isEqual); - useEffect(() => { - if (isSmartTransaction) { - if (insufficientTokens) { - dispatch(setBalanceError(true)); - } else if (balanceError && !insufficientTokens) { - dispatch(setBalanceError(false)); - } - } else if (insufficientTokens || insufficientEth) { - dispatch(setBalanceError(true)); - } else if (balanceError && !insufficientTokens && !insufficientEth) { - dispatch(setBalanceError(false)); - } - }, [ - insufficientTokens, - insufficientEth, - balanceError, - dispatch, - isSmartTransaction, - ]); - - useEffect(() => { - const currentTime = Date.now(); - const timeSinceLastFetched = currentTime - quotesLastFetched; - if ( - timeSinceLastFetched > swapsQuoteRefreshTime && - !dispatchedSafeRefetch - ) { - setDispatchedSafeRefetch(true); - dispatch(safeRefetchQuotes()); - } else if (timeSinceLastFetched > swapsQuoteRefreshTime) { - dispatch(setSwapsErrorKey(QUOTES_EXPIRED_ERROR)); - history.push(SWAPS_ERROR_ROUTE); - } - }, [ - quotesLastFetched, - dispatchedSafeRefetch, - dispatch, - history, - swapsQuoteRefreshTime, - ]); - - useEffect(() => { - if (!originalApproveAmount && approveAmount) { - setOriginalApproveAmount(approveAmount); - } - }, [originalApproveAmount, approveAmount]); - - // If it's not a Smart Transaction and ETH balance is needed, we want to show a warning. - const isNotStxAndEthBalanceIsNeeded = - (!currentSmartTransactionsEnabled || !smartTransactionsOptInStatus) && - ethBalanceNeeded; - - // If it's a Smart Transaction and ETH balance is needed, we want to show a warning. - const isStxAndEthBalanceIsNeeded = isSmartTransaction && ethBalanceNeededStx; - - // Indicates if we should show to a user a warning about insufficient funds for swapping. - const showInsufficientWarning = - (balanceError || - tokenBalanceNeeded || - isNotStxAndEthBalanceIsNeeded || - isStxAndEthBalanceIsNeeded) && - !warningHidden; - - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - - const numberOfQuotes = Object.values(quotes).length; - const bestQuoteReviewedEventSent = useRef(); - const eventObjectBase = useMemo(() => { - return { - token_from: sourceTokenSymbol, - token_from_amount: sourceTokenValue, - token_to: destinationTokenSymbol, - token_to_amount: destinationTokenValue, - request_type: fetchParams?.balanceError, - slippage: fetchParams?.slippage, - custom_slippage: fetchParams?.slippage !== 2, - response_time: fetchParams?.responseTime, - best_quote_source: topQuote?.aggregator, - available_quotes: numberOfQuotes, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }; - }, [ - sourceTokenSymbol, - sourceTokenValue, - destinationTokenSymbol, - destinationTokenValue, - fetchParams?.balanceError, - fetchParams?.slippage, - fetchParams?.responseTime, - topQuote?.aggregator, - numberOfQuotes, - hardwareWalletUsed, - hardwareWalletType, - smartTransactionsEnabled, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - ]); - - const trackAllAvailableQuotesOpened = () => { - trackEvent({ - event: 'All Available Quotes Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - other_quote_selected: usedQuote?.aggregator !== topQuote?.aggregator, - other_quote_selected_source: - usedQuote?.aggregator === topQuote?.aggregator - ? null - : usedQuote?.aggregator, - }, - }); - }; - const trackQuoteDetailsOpened = () => { - trackEvent({ - event: 'Quote Details Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - other_quote_selected: usedQuote?.aggregator !== topQuote?.aggregator, - other_quote_selected_source: - usedQuote?.aggregator === topQuote?.aggregator - ? null - : usedQuote?.aggregator, - }, - }); - }; - const trackEditSpendLimitOpened = () => { - trackEvent({ - event: 'Edit Spend Limit Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - custom_spend_limit_set: originalApproveAmount === approveAmount, - custom_spend_limit_amount: - originalApproveAmount === approveAmount ? null : approveAmount, - }, - }); - }; - const trackBestQuoteReviewedEvent = useCallback(() => { - trackEvent({ - event: 'Best Quote Reviewed', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - network_fees: feeInFiat, - }, - }); - }, [trackEvent, eventObjectBase, feeInFiat]); - const trackViewQuotePageLoadedEvent = useCallback(() => { - trackEvent({ - event: 'View Quote Page Loaded', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - response_time: currentTimestamp - reviewSwapClickedTimestamp, - }, - }); - }, [ - trackEvent, - eventObjectBase, - currentTimestamp, - reviewSwapClickedTimestamp, - ]); - - useEffect(() => { - if ( - !bestQuoteReviewedEventSent.current && - [ - sourceTokenSymbol, - sourceTokenValue, - destinationTokenSymbol, - destinationTokenValue, - fetchParams, - topQuote, - numberOfQuotes, - feeInFiat, - ].every((dep) => dep !== null && dep !== undefined) - ) { - bestQuoteReviewedEventSent.current = true; - trackBestQuoteReviewedEvent(); - } - }, [ - fetchParams, - topQuote, - numberOfQuotes, - feeInFiat, - destinationTokenSymbol, - destinationTokenValue, - sourceTokenSymbol, - sourceTokenValue, - trackBestQuoteReviewedEvent, - ]); - - const metaMaskFee = usedQuote.fee; - - /* istanbul ignore next */ - const onFeeCardTokenApprovalClick = () => { - trackEditSpendLimitOpened(); - dispatch( - showModal({ - name: 'EDIT_APPROVAL_PERMISSION', - decimals: selectedFromToken.decimals, - origin: 'MetaMask', - setCustomAmount: (newCustomPermissionAmount) => { - const customPermissionAmount = - newCustomPermissionAmount === '' - ? originalApproveAmount - : newCustomPermissionAmount; - const newData = getCustomTxParamsData(approveTxParams.data, { - customPermissionAmount, - decimals: selectedFromToken.decimals, - }); - - if ( - customPermissionAmount?.length && - approveTxParams.data !== newData - ) { - dispatch(setCustomApproveTxData(newData)); - } - }, - tokenAmount: originalApproveAmount, - customTokenAmount: - originalApproveAmount === approveAmount ? null : approveAmount, - tokenBalance, - tokenSymbol: selectedFromToken.symbol, - requiredMinimum: calcTokenAmount( - usedQuote.sourceAmount, - selectedFromToken.decimals, - ), - }), - ); - }; - const actionableBalanceErrorMessage = tokenBalanceUnavailable - ? t('swapTokenBalanceUnavailable', [sourceTokenSymbol]) - : t('swapApproveNeedMoreTokens', [ - <span key="swapApproveNeedMoreTokens-1" className="view-quote__bold"> - {tokenBalanceNeeded || ethBalanceNeededStx || ethBalanceNeeded} - </span>, - tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) - ? sourceTokenSymbol - : defaultSwapsToken.symbol, - ]); - - // Price difference warning - const priceSlippageBucket = usedQuote?.priceSlippage?.bucket; - const lastPriceDifferenceBucket = usePrevious(priceSlippageBucket); - - // If the user agreed to a different bucket of risk, make them agree again - useEffect(() => { - if ( - acknowledgedPriceDifference && - lastPriceDifferenceBucket === GasRecommendations.medium && - priceSlippageBucket === GasRecommendations.high - ) { - setAcknowledgedPriceDifference(false); - } - }, [ - priceSlippageBucket, - acknowledgedPriceDifference, - lastPriceDifferenceBucket, - ]); - - let viewQuotePriceDifferenceComponent = null; - const priceSlippageFromSource = useEthFiatAmount( - usedQuote?.priceSlippage?.sourceAmountInETH || 0, - { showFiat: true }, - ); - const priceSlippageFromDestination = useEthFiatAmount( - usedQuote?.priceSlippage?.destinationAmountInETH || 0, - { showFiat: true }, - ); - - // We cannot present fiat value if there is a calculation error or no slippage - // from source or destination - const priceSlippageUnknownFiatValue = - !priceSlippageFromSource || - !priceSlippageFromDestination || - Boolean(usedQuote?.priceSlippage?.calculationError); - - let priceDifferencePercentage = 0; - if (usedQuote?.priceSlippage?.ratio) { - priceDifferencePercentage = parseFloat( - new BigNumber(usedQuote.priceSlippage.ratio, 10) - .minus(1, 10) - .times(100, 10) - .toFixed(2), - 10, - ); - } - - const shouldShowPriceDifferenceWarning = - !tokenBalanceUnavailable && - !showInsufficientWarning && - usedQuote && - (priceDifferenceRiskyBuckets.includes(priceSlippageBucket) || - priceSlippageUnknownFiatValue); - - if (shouldShowPriceDifferenceWarning) { - viewQuotePriceDifferenceComponent = ( - <ViewQuotePriceDifference - usedQuote={usedQuote} - sourceTokenValue={sourceTokenValue} - destinationTokenValue={destinationTokenValue} - priceSlippageFromSource={priceSlippageFromSource} - priceSlippageFromDestination={priceSlippageFromDestination} - priceDifferencePercentage={priceDifferencePercentage} - priceSlippageUnknownFiatValue={priceSlippageUnknownFiatValue} - onAcknowledgementClick={() => { - setAcknowledgedPriceDifference(true); - }} - acknowledged={acknowledgedPriceDifference} - /> - ); - } - - const disableSubmissionDueToPriceWarning = - shouldShowPriceDifferenceWarning && !acknowledgedPriceDifference; - - const isShowingWarning = - showInsufficientWarning || shouldShowPriceDifferenceWarning; - - const isSwapButtonDisabled = Boolean( - submitClicked || - balanceError || - tokenBalanceUnavailable || - disableSubmissionDueToPriceWarning || - (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || - (!networkAndAccountSupports1559 && - (gasPrice === null || gasPrice === undefined)) || - (currentSmartTransactionsEnabled && - (currentSmartTransactionsError || smartTransactionsError)) || - (currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees), - ); - - useEffect(() => { - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !insufficientTokens - ) { - const unsignedTx = { - from: unsignedTransaction.from, - to: unsignedTransaction.to, - value: unsignedTransaction.value, - data: unsignedTransaction.data, - gas: unsignedTransaction.gas, - chainId, - }; - intervalId = setInterval(() => { - if (!swapsSTXLoading) { - dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction: unsignedTx, - approveTxParams, - fallbackOnNotEnoughFunds: false, - }), - ); - } - }, swapsNetworkConfig.stxGetTransactionsRefreshTime); - dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction: unsignedTx, - approveTxParams, - fallbackOnNotEnoughFunds: false, - }), - ); - } else if (intervalId) { - clearInterval(intervalId); - } - return () => clearInterval(intervalId); - // eslint-disable-next-line - }, [ - dispatch, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - unsignedTransaction.data, - unsignedTransaction.from, - unsignedTransaction.value, - unsignedTransaction.gas, - unsignedTransaction.to, - chainId, - swapsNetworkConfig.stxGetTransactionsRefreshTime, - insufficientTokens, - ]); - - useEffect(() => { - // Thanks to the next line we will only do quotes polling 3 times before showing a Quote Timeout modal. - dispatch(setSwapsQuotesPollingLimitEnabled(true)); - if (reviewSwapClickedTimestamp) { - trackViewQuotePageLoadedEvent(); - } - }, [dispatch, trackViewQuotePageLoadedEvent, reviewSwapClickedTimestamp]); - - useEffect(() => { - // if smart transaction error is turned off, reset submit clicked boolean - if ( - !currentSmartTransactionsEnabled && - currentSmartTransactionsError && - submitClicked - ) { - setSubmitClicked(false); - } - }, [ - currentSmartTransactionsEnabled, - currentSmartTransactionsError, - submitClicked, - ]); - - useEffect(() => { - if (!usedQuote?.multiLayerL1TradeFeeTotal) { - return; - } - const getEstimatedL1Fees = async () => { - try { - let l1ApprovalFeeTotal = '0x0'; - if (approveTxParams) { - l1ApprovalFeeTotal = await dispatch( - getLayer1GasFee({ - transactionParams: { - ...approveTxParams, - gasPrice: addHexPrefix(approveTxParams.gasPrice), - value: '0x0', // For approval txs we need to use "0x0" here. - }, - chainId, - }), - ); - setMultiLayerL1ApprovalFeeTotal(l1ApprovalFeeTotal); - } - const l1FeeTotal = sumHexes( - usedQuote.multiLayerL1TradeFeeTotal, - l1ApprovalFeeTotal, - ); - setMultiLayerL1FeeTotal(l1FeeTotal); - } catch (e) { - captureException(e); - setMultiLayerL1FeeTotal(null); - setMultiLayerL1ApprovalFeeTotal(null); - } - }; - getEstimatedL1Fees(); - }, [unsignedTransaction, approveTxParams, chainId, usedQuote]); - - useEffect(() => { - if (isSmartTransaction) { - // Removes a smart transactions error when the component loads. - dispatch({ - type: SET_SMART_TRANSACTIONS_ERROR, - payload: null, - }); - } - }, [isSmartTransaction, dispatch]); - - return ( - <div className="view-quote"> - <div - className={classnames('view-quote__content', { - 'view-quote__content_modal': disableSubmissionDueToPriceWarning, - })} - > - { - /* istanbul ignore next */ - selectQuotePopoverShown && ( - <SelectQuotePopover - quoteDataRows={renderablePopoverData} - onClose={() => setSelectQuotePopoverShown(false)} - onSubmit={(aggId) => dispatch(swapsQuoteSelected(aggId))} - swapToSymbol={destinationTokenSymbol} - initialAggId={usedQuote.aggregator} - onQuoteDetailsIsOpened={trackQuoteDetailsOpened} - hideEstimatedGasFee={ - smartTransactionsEnabled && smartTransactionsOptInStatus - } - /> - ) - } - - <div - className={classnames('view-quote__warning-wrapper', { - 'view-quote__warning-wrapper--thin': !isShowingWarning, - })} - > - {viewQuotePriceDifferenceComponent} - {(showInsufficientWarning || tokenBalanceUnavailable) && ( - <ActionableMessage - message={actionableBalanceErrorMessage} - onClose={ - /* istanbul ignore next */ - () => setWarningHidden(true) - } - /> - )} - </div> - <div className="view-quote__countdown-timer-container"> - <CountdownTimer - timeStarted={quotesLastFetched} - warningTime="0:10" - labelKey="swapNewQuoteIn" - /> - </div> - <MainQuoteSummary - sourceValue={calcTokenValue(sourceTokenValue, sourceTokenDecimals)} - sourceDecimals={sourceTokenDecimals} - sourceSymbol={sourceTokenSymbol} - destinationValue={calcTokenValue( - destinationTokenValue, - destinationTokenDecimals, - )} - destinationDecimals={destinationTokenDecimals} - destinationSymbol={destinationTokenSymbol} - sourceIconUrl={sourceTokenIconUrl} - destinationIconUrl={destinationIconUrl} - /> - {currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees && - !showInsufficientWarning && ( - <Box marginTop={0} marginBottom={10}> - <PulseLoader /> - </Box> - )} - {(!currentSmartTransactionsEnabled || - !smartTransactionsOptInStatus || - smartTransactionFees?.tradeTxFees) && ( - <div - className={classnames('view-quote__fee-card-container', { - 'view-quote__fee-card-container--three-rows': - approveTxParams && (!balanceError || warningHidden), - })} - > - <FeeCard - primaryFee={{ - fee: feeInEth, - maxFee: maxFeeInEth, - }} - secondaryFee={{ - fee: feeInFiat, - maxFee: maxFeeInFiat, - }} - hideTokenApprovalRow={ - !approveTxParams || (balanceError && !warningHidden) - } - tokenApprovalSourceTokenSymbol={sourceTokenSymbol} - onTokenApprovalClick={onFeeCardTokenApprovalClick} - metaMaskFee={String(metaMaskFee)} - numberOfQuotes={Object.values(quotes).length} - onQuotesClick={ - /* istanbul ignore next */ - () => { - trackAllAvailableQuotesOpened(); - setSelectQuotePopoverShown(true); - } - } - maxPriorityFeePerGasDecGWEI={hexWEIToDecGWEI( - maxPriorityFeePerGas, - )} - maxFeePerGasDecGWEI={hexWEIToDecGWEI(maxFeePerGas)} - /> - </div> - )} - </div> - <SwapsFooter - onSubmit={ - /* istanbul ignore next */ () => { - setSubmitClicked(true); - if (!balanceError) { - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees - ) { - dispatch( - signAndSendSwapsSmartTransaction({ - unsignedTransaction, - trackEvent, - history, - additionalTrackingParams, - }), - ); - } else { - dispatch( - signAndSendTransactions( - history, - trackEvent, - additionalTrackingParams, - ), - ); - } - } else if (destinationToken.symbol === defaultSwapsToken.symbol) { - history.push(DEFAULT_ROUTE); - } else { - history.push(`${ASSET_ROUTE}/${destinationToken.address}`); - } - } - } - submitText={ - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - swapsSTXLoading - ? t('preparingSwap') - : t('swap') - } - hideCancel - disabled={isSwapButtonDisabled} - className={isShowingWarning ? 'view-quote__thin-swaps-footer' : ''} - showTopBorder - /> - </div> - ); -} diff --git a/ui/pages/swaps/view-quote/view-quote.test.js b/ui/pages/swaps/view-quote/view-quote.test.js deleted file mode 100644 index 3193a54dfb35..000000000000 --- a/ui/pages/swaps/view-quote/view-quote.test.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { NetworkType } from '@metamask/controller-utils'; -import { NetworkStatus } from '@metamask/network-controller'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - renderWithProvider, - createSwapsMockStore, - MOCKS, -} from '../../../../test/jest'; - -import ViewQuote from '.'; - -jest.mock( - '../../../components/ui/info-tooltip/info-tooltip-icon', - () => () => '<InfoTooltipIcon />', -); - -jest.mock('../../confirmations/hooks/useGasFeeInputs', () => { - return { - useGasFeeInputs: () => { - return { - maxFeePerGas: 16, - maxPriorityFeePerGas: 3, - gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(), - }; - }, - }; -}); - -const middleware = [thunk]; -const createProps = (customProps = {}) => { - return { - inputValue: '5', - onInputChange: jest.fn(), - ethBalance: '6 ETH', - setMaxSlippage: jest.fn(), - maxSlippage: 15, - selectedAccountAddress: 'selectedAccountAddress', - isFeatureFlagLoaded: false, - ...customProps, - }; -}; - -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - safeRefetchQuotes: jest.fn(), - setSwapsErrorKey: jest.fn(), - updateTransaction: jest.fn(), - getGasFeeTimeEstimate: jest.fn(), - setSwapsQuotesPollingLimitEnabled: jest.fn(), -}); - -describe('ViewQuote', () => { - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - <ViewQuote {...props} />, - store, - ); - expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot(); - expect( - getByTestId('main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - expect(getByText('Estimated gas fee')).toBeInTheDocument(); - expect(getByText('0.00008 ETH')).toBeInTheDocument(); - expect(getByText('Max fee')).toBeInTheDocument(); - expect(getByText('Swap')).toBeInTheDocument(); - }); - - it('renders the component with EIP-1559 enabled', () => { - const state = createSwapsMockStore(); - state.metamask.selectedNetworkClientId = NetworkType.mainnet; - state.metamask.networksMetadata = { - [NetworkType.mainnet]: { - EIPS: { 1559: true }, - status: NetworkStatus.Available, - }, - }; - const store = configureMockStore(middleware)(state); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - <ViewQuote {...props} />, - store, - ); - expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot(); - expect( - getByTestId('main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - expect(getByText('Estimated gas fee')).toBeInTheDocument(); - expect(getByText('0.00008 ETH')).toBeInTheDocument(); - expect(getByText('Max fee')).toBeInTheDocument(); - expect(getByText('Swap')).toBeInTheDocument(); - }); -}); diff --git a/ui/pages/token-details/index.js b/ui/pages/token-details/index.js deleted file mode 100644 index 0a8a820e90fe..000000000000 --- a/ui/pages/token-details/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './token-details-page'; diff --git a/ui/pages/token-details/index.scss b/ui/pages/token-details/index.scss deleted file mode 100644 index 6751544d7506..000000000000 --- a/ui/pages/token-details/index.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use "design-system"; - -.token-details { - &__title { - text-transform: capitalize; - } - - &__closeButton { - float: right; - width: 10px; - margin-top: -17px; - margin-inline-end: -8px; - - &::after { - font-size: 24px; - content: '\00D7'; - color: var(--color-icon-default); - } - } - - &__token-value { - font-size: 32px; - } - - &__token-address { - width: 222px; - } - - &__copy-icon { - float: right; - margin-inline-start: 62px; - - @include design-system.screen-sm-min { - margin-inline-start: 112px; - } - } - - &__hide-token-button { - width: 319px; - height: 39px; - margin-top: 70px; - - @include design-system.screen-sm-min { - margin-inline-start: 20px; - } - } -} diff --git a/ui/pages/token-details/token-details-page.js b/ui/pages/token-details/token-details-page.js deleted file mode 100644 index 086a2de61f4b..000000000000 --- a/ui/pages/token-details/token-details-page.js +++ /dev/null @@ -1,238 +0,0 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Redirect, useHistory, useParams } from 'react-router-dom'; -import { getProviderConfig, getTokens } from '../../ducks/metamask/metamask'; -import { getTokenList } from '../../selectors'; -import { useCopyToClipboard } from '../../hooks/useCopyToClipboard'; -import Identicon from '../../components/ui/identicon'; -import { I18nContext } from '../../contexts/i18n'; -import { useTokenTracker } from '../../hooks/useTokenTracker'; -import { useTokenFiatAmount } from '../../hooks/useTokenFiatAmount'; -import { showModal } from '../../store/actions'; -import { NETWORK_TYPES } from '../../../shared/constants/network'; -import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'; -import Tooltip from '../../components/ui/tooltip'; -import Button from '../../components/ui/button'; -import Box from '../../components/ui/box'; -import { - TextVariant, - FontWeight, - DISPLAY, - TextAlign, - OverflowWrap, - TextColor, - IconColor, -} from '../../helpers/constants/design-system'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; -import { - ButtonIcon, - ButtonIconSize, - IconName, - Text, -} from '../../components/component-library'; - -export default function TokenDetailsPage() { - const dispatch = useDispatch(); - const history = useHistory(); - const t = useContext(I18nContext); - const tokens = useSelector(getTokens); - const tokenList = useSelector(getTokenList); - const { address: tokenAddress } = useParams(); - const tokenMetadata = tokenList[tokenAddress.toLowerCase()]; - const aggregators = tokenMetadata?.aggregators?.join(', '); - - const token = tokens.find(({ address }) => - isEqualCaseInsensitive(address, tokenAddress), - ); - - // When the user did not import the token - // the token variable will be undefined. - // In that case we want to call useTokenTracker with [] instead of [undefined] - const { tokensWithBalances } = useTokenTracker({ - tokens: token ? [token] : [], - }); - const tokenBalance = tokensWithBalances[0]?.string; - - const tokenCurrencyBalance = useTokenFiatAmount( - token?.address, - tokenBalance, - token?.symbol, - ); - - const { nickname, type: networkType } = useSelector(getProviderConfig); - - const [copied, handleCopy] = useCopyToClipboard(); - - if (!token) { - return <Redirect to={{ pathname: DEFAULT_ROUTE }} />; - } - return ( - <Box className="page-container token-details"> - <Box marginLeft={5} marginRight={6}> - <Text - fontWeight={FontWeight.Bold} - margin={0} - marginTop={4} - variant={TextVariant.bodySm} - as="h6" - color={TextColor.textDefault} - className="token-details__title" - > - {t('tokenDetails')} - <Button - type="link" - onClick={() => history.push(`${ASSET_ROUTE}/${token.address}`)} - className="token-details__closeButton" - /> - </Text> - <Box display={DISPLAY.FLEX} marginTop={4}> - <Text - align={TextAlign.Center} - fontWeight={FontWeight.Bold} - margin={0} - marginRight={5} - variant={TextVariant.headingSm} - as="h4" - color={TextColor.textDefault} - className="token-details__token-value" - > - {tokenBalance || ''} - </Text> - <Box marginTop={1}> - <Identicon - diameter={32} - address={token.address} - image={tokenMetadata ? tokenMetadata.iconUrl : token.image} - /> - </Box> - </Box> - <Text - margin={0} - marginTop={4} - variant={TextVariant.bodySm} - as="h6" - color={TextColor.textAlternative} - > - {tokenCurrencyBalance || ''} - </Text> - <Text - margin={0} - marginTop={6} - variant={TextVariant.bodyXs} - as="h6" - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('tokenContractAddress')} - </Text> - <Box display={DISPLAY.FLEX}> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={0} - marginTop={2} - color={TextColor.textDefault} - overflowWrap={OverflowWrap.BreakWord} - className="token-details__token-address" - > - {token.address} - </Text> - <Tooltip - position="bottom" - title={copied ? t('copiedExclamation') : t('copyToClipboard')} - containerClassName="token-details__copy-icon" - > - <ButtonIcon - ariaLabel="copy" - name={copied ? IconName.CopySuccess : IconName.Copy} - className="token-details__copyIcon" - onClick={() => handleCopy(token.address)} - color={IconColor.primaryDefault} - size={ButtonIconSize.Sm} - /> - </Tooltip> - </Box> - <Text - variant={TextVariant.bodyXs} - as="h6" - margin={0} - marginTop={4} - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('tokenDecimalTitle')} - </Text> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={0} - marginTop={1} - color={TextColor.textDefault} - > - {token.decimals} - </Text> - <Text - variant={TextVariant.bodyXs} - as="h6" - margin={0} - marginTop={4} - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('network')} - </Text> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={1} - marginTop={0} - color={TextColor.textDefault} - > - {networkType === NETWORK_TYPES.RPC - ? nickname ?? t('privateNetwork') - : t(networkType)} - </Text> - {aggregators && ( - <> - <Text - variant={TextVariant.bodyXs} - as="h6" - margin={0} - marginTop={4} - color={TextColor.textAlternative} - fontWeight={FontWeight.Bold} - > - {t('tokenList')}: - </Text> - <Text - variant={TextVariant.bodySm} - as="h6" - margin={0} - marginTop={1} - color={TextColor.textDefault} - > - {`${aggregators}.`} - </Text> - </> - )} - <Button - type="secondary" - className="token-details__hide-token-button" - onClick={() => { - dispatch( - showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token, history }), - ); - }} - > - <Text - variant={TextVariant.bodySm} - as="h6" - color={TextColor.primaryDefault} - > - {t('hideToken')} - </Text> - </Button> - </Box> - </Box> - ); -} diff --git a/ui/pages/token-details/token-details-page.test.js b/ui/pages/token-details/token-details-page.test.js deleted file mode 100644 index c7d7933c6c3b..000000000000 --- a/ui/pages/token-details/token-details-page.test.js +++ /dev/null @@ -1,370 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { EthAccountType } from '@metamask/keyring-api'; -import { fireEvent } from '@testing-library/react'; -import { renderWithProvider } from '../../../test/lib/render-helpers'; -import Identicon from '../../components/ui/identicon'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; -import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; -import { mockNetworkState } from '../../../test/stub/networks'; -import { CHAIN_IDS } from '../../../shared/constants/network'; -import TokenDetailsPage from './token-details-page'; - -const testTokenAddress = '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F'; -const state = { - metamask: { - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0xAddress', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - contractExchangeRates: { - '0xAnotherToken': 0.015, - }, - useTokenDetection: true, - tokenList: { - '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': { - address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Synthetix', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', - name: 'Synthetix Network Token', - occurrences: 12, - symbol: 'SNX', - }, - '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: - 'https://images.prismic.io/token-price-prod/d0352dd9-5de8-4633-839d-bc3422c44d9c_UNI%404x.png', - name: 'Uniswap', - occurrences: 11, - symbol: 'UNI', - }, - '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e': { - address: '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: - 'https://images.prismic.io/token-price-prod/917bc4fa-59d4-40f5-a3ef-33035698ffe0_YFIxxxhdpi.png', - name: 'yearn.finance', - occurrences: 11, - symbol: 'YFI', - }, - '0x408e41876cccdc0f92210600ef50372656052a38': { - address: '0x408e41876cccdc0f92210600ef50372656052a38', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/REN/color_icon.png', - name: 'Republic Token', - occurrences: 11, - symbol: 'REN', - }, - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599': { - address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 8, - iconUrl: - 'https://images.prismic.io/token-price-prod/c27778b1-f402-45f0-9225-f24f24b0518a_WBTC-xxxhdpi.png', - name: 'Wrapped BTC', - occurrences: 11, - symbol: 'WBTC', - }, - '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2': { - address: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/MKR/color_icon.png', - name: 'MakerDAO', - occurrences: 11, - symbol: 'MKR', - }, - '0x514910771af9ca656af840dff83e8264ecf986ca': { - address: '0x514910771af9ca656af840dff83e8264ecf986ca', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/LINK/color_icon.png', - name: 'ChainLink Token', - occurrences: 11, - symbol: 'LINK', - }, - '0x6b175474e89094c44da98b954eedeac495271d0f': { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - aggregators: [ - 'Aave', - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: 'https://crypto.com/price/coin-data/icon/DAI/color_icon.png', - name: 'Dai Stablecoin', - occurrences: 11, - symbol: 'DAI', - }, - '0x04fa0d235c4abf4bcf4787af4cf447de572ef828': { - address: '0x04fa0d235c4abf4bcf4787af4cf447de572ef828', - aggregators: [ - 'Bancor', - 'CMC', - 'Crypto.com', - 'CoinGecko', - '1inch', - 'Paraswap', - 'PMM', - 'Zapper', - 'Zerion', - '0x', - ], - decimals: 18, - iconUrl: - 'https://images.prismic.io/token-price-prod/e2850554-ccf6-4514-9c3c-a17e19dea82f_UMAxxxhdpi.png', - name: 'UMA', - occurrences: 10, - symbol: 'UMA', - }, - }, - ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - currencyRates: {}, - preferences: { - showFiatInTestnets: true, - }, - tokens: [ - { - address: testTokenAddress, - symbol: 'SNX', - decimals: 18, - image: 'testImage', - isERC721: false, - }, - { - address: '0xaD6D458402F60fD3Bd25163575031ACDce07538U', - symbol: 'DAU', - decimals: 18, - image: null, - isERC721: false, - }, - ], - }, -}; - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - return { - ...original, - useHistory: () => ({ - push: jest.fn(), - }), - useParams: () => ({ - address: testTokenAddress, - }), - }; -}); - -describe('TokenDetailsPage', () => { - it('should render title "Token details" in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token details')).toBeInTheDocument(); - }); - - it('should close token details page when close button is clicked', () => { - const store = configureMockStore()(state); - const { container } = renderWithProvider(<TokenDetailsPage />, store); - const onCloseBtn = container.querySelector('.token-details__closeButton'); - fireEvent.click(onCloseBtn); - expect(onCloseBtn).toBeDefined(); - }); - - it('should render an icon image', () => { - const token = state.metamask.tokens.find(({ address }) => - isEqualCaseInsensitive(address, testTokenAddress), - ); - const iconImage = ( - <Identicon diameter={32} address={testTokenAddress} image={token.image} /> - ); - expect(iconImage).toBeDefined(); - }); - - it('should render token contract address title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token contract address')).toBeInTheDocument(); - }); - - it('should render token contract address in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText(testTokenAddress)).toBeInTheDocument(); - }); - - it('should call copy button when click is simulated', () => { - const store = configureMockStore()(state); - const { container } = renderWithProvider(<TokenDetailsPage />, store); - const handleCopyBtn = container.querySelector('.token-details__copyIcon'); - fireEvent.click(handleCopyBtn); - expect(handleCopyBtn).toBeDefined(); - }); - - it('should render token decimal title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token decimal:')).toBeInTheDocument(); - }); - - it('should render number of token decimals in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('18')).toBeInTheDocument(); - }); - - it('should render current network title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Network:')).toBeInTheDocument(); - }); - - it('should render current network in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Ethereum Mainnet')).toBeInTheDocument(); - }); - - it('should render token list title in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Token lists:')).toBeInTheDocument(); - }); - - it('should render token list for the token in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect( - getByText( - 'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.', - ), - ).toBeInTheDocument(); - }); - - it('should call hide token button when button is clicked in token details page', () => { - const store = configureMockStore()(state); - const { container } = renderWithProvider(<TokenDetailsPage />, store); - const hideTokenBtn = container.querySelector( - '.token-details__hide-token-button', - ); - fireEvent.click(hideTokenBtn); - expect(hideTokenBtn).toBeDefined(); - }); - - it('should render label of hide token button in token details page', () => { - const store = configureMockStore()(state); - const { getByText } = renderWithProvider(<TokenDetailsPage />, store); - expect(getByText('Hide token')).toBeInTheDocument(); - }); -}); diff --git a/ui/selectors/accounts.test.ts b/ui/selectors/accounts.test.ts index 22fd6f610b12..61a0059989ba 100644 --- a/ui/selectors/accounts.test.ts +++ b/ui/selectors/accounts.test.ts @@ -147,42 +147,4 @@ describe('Accounts Selectors', () => { expect(isSelectedInternalAccountBtc(state)).toBe(false); }); }); - - describe('hasCreatedBtcTestnetAccount', () => { - it('returns true if the BTC testnet account has been created', () => { - const state: AccountsState = { - metamask: { - // No-op for this test, but might be required in the future: - ...MOCK_STATE.metamask, - internalAccounts: { - selectedAccount: MOCK_ACCOUNT_BIP122_P2WPKH.id, - accounts: { - mock_account_bip122_pwpkh: MOCK_ACCOUNT_BIP122_P2WPKH, - mock_account_bip122_p2wpkh_testnet: - MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, - }, - }, - }, - }; - - expect(hasCreatedBtcTestnetAccount(state)).toBe(true); - }); - - it('returns false if the BTC testnet account has not been created yet', () => { - const state: AccountsState = { - metamask: { - // No-op for this test, but might be required in the future: - ...MOCK_STATE.metamask, - internalAccounts: { - selectedAccount: MOCK_ACCOUNT_BIP122_P2WPKH.id, - accounts: { - mock_account_bip122_p2wpkh: MOCK_ACCOUNT_BIP122_P2WPKH, - }, - }, - }, - }; - - expect(isSelectedInternalAccountBtc(state)).toBe(false); - }); - }); }); 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..b676da209046 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -231,8 +231,11 @@ export function getMultichainProviderConfig( return getMultichainNetwork(state, account).network; } -export function getMultichainCurrentNetwork(state: MultichainState) { - return getMultichainProviderConfig(state); +export function getMultichainCurrentNetwork( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainProviderConfig(state, account); } export function getMultichainNativeCurrency( @@ -244,10 +247,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 +262,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..431d2c1b5d0a 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -67,6 +67,7 @@ import { shortenAddress, getAccountByAddress, getURLHostName, + sortSelectedInternalAccounts, } from '../helpers/utils/util'; import { @@ -108,6 +109,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, @@ -387,6 +389,23 @@ export function getInternalAccount(state, accountId) { return state.metamask.internalAccounts.accounts[accountId]; } +export const getEvmInternalAccounts = createSelector( + getInternalAccounts, + (accounts) => { + return accounts.filter((account) => isEvmAccountType(account.type)); + }, +); + +export const getSelectedEvmInternalAccount = createSelector( + getEvmInternalAccounts, + (accounts) => { + // We should always have 1 EVM account (if not, it would be `undefined`, same + // as `getSelectedInternalAccount` selector. + const [evmAccountSelected] = sortSelectedInternalAccounts(accounts); + return evmAccountSelected; + }, +); + /** * Returns an array of internal accounts sorted by keyring. * @@ -537,6 +556,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 +1362,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 +1533,11 @@ export const getConnectedSitesList = createDeepEqualSelector( }, ); +export function getShouldShowAggregatedBalancePopover(state) { + const { shouldShowAggregatedBalancePopover } = getPreferences(state); + return shouldShowAggregatedBalancePopover; +} + export const getConnectedSnapsList = createDeepEqualSelector( getSnapsList, (snapsData) => { @@ -1566,6 +1612,7 @@ export const getSnapsMetadata = createDeepEqualSelector( snapsMetadata[snapId] = { name: manifest.proposedName, description: manifest.description, + hidden: snap.hidden, }; return snapsMetadata; }, {}); @@ -1978,6 +2025,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 +2617,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..8d71048e0924 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1,6 +1,10 @@ import { deepClone } from '@metamask/snaps-utils'; import { ApprovalType } from '@metamask/controller-utils'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { + BtcAccountType, + EthAccountType, + EthMethod, +} from '@metamask/keyring-api'; import { TransactionStatus } from '@metamask/transaction-controller'; import mockState from '../../test/data/mock-state.json'; import { KeyringType } from '../../shared/constants/keyring'; @@ -11,6 +15,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', () => ({ @@ -35,6 +40,21 @@ const modifyStateWithHWKeyring = (keyring) => { return modifiedState; }; +const mockAccountsState = (accounts) => { + const accountsMap = accounts.reduce((map, account) => { + map[account.id] = account; + return map; + }, {}); + + return { + metamask: { + internalAccounts: { + accounts: accountsMap, + }, + }, + }; +}; + describe('Selectors', () => { describe('#getSelectedAddress', () => { it('returns undefined if selectedAddress is undefined', () => { @@ -2018,4 +2038,160 @@ 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'); + }); + }); + + describe('getEvmInternalAccounts', () => { + const account1 = createMockInternalAccount({ + keyringType: KeyringType.hd, + }); + const account2 = createMockInternalAccount({ + type: EthAccountType.Erc4337, + keyringType: KeyringType.snap, + }); + const account3 = createMockInternalAccount({ + keyringType: KeyringType.imported, + }); + const account4 = createMockInternalAccount({ + keyringType: KeyringType.ledger, + }); + const account5 = createMockInternalAccount({ + keyringType: KeyringType.trezor, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + + const evmAccounts = [account1, account2, account3, account4, account5]; + + it('returns all EVM accounts when only EVM accounts are present', () => { + const state = mockAccountsState(evmAccounts); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('only returns EVM accounts when there are non-EVM accounts', () => { + const state = mockAccountsState([ + ...evmAccounts, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('returns an empty array when there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual([]); + }); + }); + + describe('getSelectedEvmInternalAccount', () => { + const account1 = createMockInternalAccount({ + lastSelected: 1, + }); + const account2 = createMockInternalAccount({ + lastSelected: 2, + }); + const account3 = createMockInternalAccount({ + lastSelected: 3, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 4, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 5, + }); + + it('returns the last selected EVM account', () => { + const state = mockAccountsState([account1, account2, account3]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns the last selected EVM account when there are non-EVM accounts', () => { + const state = mockAccountsState([ + account1, + account2, + account3, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns `undefined` if there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(undefined); + }); + }); }); 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..d86ea20f845c 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', () => { @@ -475,6 +483,50 @@ describe('Actions', () => { }); }); + describe('#getDeviceNameForMetric', () => { + const deviceName = 'ledger'; + const hdPath = "m/44'/60'/0'/0/0"; + + afterEach(() => { + sinon.restore(); + }); + + it('calls getDeviceNameForMetric in background', async () => { + const store = mockStore(); + + const mockGetDeviceName = background.getDeviceNameForMetric.callsFake( + (_, __, cb) => cb(), + ); + + setBackgroundConnection(background); + + await store.dispatch(actions.getDeviceNameForMetric(deviceName, hdPath)); + expect(mockGetDeviceName.callCount).toStrictEqual(1); + }); + + it('shows loading indicator and displays error', async () => { + const store = mockStore(); + + background.getDeviceNameForMetric.callsFake((_, __, cb) => + cb(new Error('error')), + ); + + setBackgroundConnection(background); + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', payload: undefined }, + { type: 'DISPLAY_WARNING', payload: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, + ]; + + await expect( + store.dispatch(actions.getDeviceNameForMetric(deviceName, hdPath)), + ).rejects.toThrow('error'); + + expect(store.getActions()).toStrictEqual(expectedActions); + }); + }); + describe('#forgetDevice', () => { afterEach(() => { sinon.restore(); @@ -2530,4 +2582,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..a051bb15d5fd 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -9,7 +9,8 @@ import { captureException } from '@sentry/browser'; import { capitalize, isEqual } from 'lodash'; import { ThunkAction } from 'redux-thunk'; import { Action, AnyAction } from 'redux'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; +import type { DataWithOptionalCause } from '@metamask/rpc-errors'; import type { Hex, Json } from '@metamask/utils'; import { AssetsContractController, @@ -28,6 +29,7 @@ import { UpdateProposedNamesResult, } from '@metamask/name-controller'; import { + GasFeeEstimates, TransactionMeta, TransactionParams, TransactionType, @@ -41,6 +43,7 @@ import { InterfaceState } from '@metamask/snaps-sdk'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { NotificationServicesController } from '@metamask/notification-services-controller'; import { Patch } from 'immer'; +import { HandlerType } from '@metamask/snaps-utils'; import switchDirection from '../../shared/lib/switch-direction'; import { ENVIRONMENT_TYPE_NOTIFICATION, @@ -101,7 +104,7 @@ import { } from '../../shared/constants/metametrics'; import { parseSmartTransactionsError } from '../pages/swaps/swaps.util'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; -import { getSmartTransactionsOptInStatus } from '../../shared/modules/selectors'; +import { getSmartTransactionsOptInStatusInternal } from '../../shared/modules/selectors'; import { NOTIFICATIONS_EXPIRATION_DELAY } from '../helpers/constants/notifications'; import { fetchLocale, @@ -110,6 +113,7 @@ import { import { decimalToHex } from '../../shared/modules/conversion.utils'; import { TxGasFees, PriorityLevels } from '../../shared/constants/gas'; import { + getErrorMessage, isErrorWithMessage, logErrorWithMessage, } from '../../shared/modules/error'; @@ -119,6 +123,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'; @@ -174,7 +183,7 @@ export function tryUnlockMetamask( dispatch(hideLoadingIndication()); }) .catch((err) => { - dispatch(unlockFailed(err.message)); + dispatch(unlockFailed(getErrorMessage(err))); dispatch(hideLoadingIndication()); return Promise.reject(err); }); @@ -222,7 +231,7 @@ export function createNewVaultAndRestore( dispatch(hideLoadingIndication()); }) .catch((err) => { - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); dispatch(hideLoadingIndication()); return Promise.reject(err); }); @@ -242,7 +251,7 @@ export function createNewVaultAndGetSeedPhrase( } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -266,7 +275,7 @@ export function unlockAndGetSeedPhrase( } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -369,7 +378,7 @@ export function resetAccount(): ThunkAction< dispatch(hideLoadingIndication()); if (err) { if (isErrorWithMessage(err)) { - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); } reject(err); return; @@ -499,6 +508,33 @@ export function checkHardwareStatus( }; } +export function getDeviceNameForMetric( + deviceName: HardwareDeviceNames, + hdPath: string, +): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { + log.debug(`background.getDeviceNameForMetric`, deviceName, hdPath); + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(showLoadingIndication()); + + let result: string; + try { + result = await submitRequestToBackground<string>( + 'getDeviceNameForMetric', + [deviceName, hdPath], + ); + } catch (error) { + logErrorWithMessage(error); + dispatch(displayWarning(error)); + throw error; + } finally { + dispatch(hideLoadingIndication()); + } + + await forceUpdateMetamaskState(dispatch); + return result; + }; +} + export function forgetDevice( deviceName: HardwareDeviceNames, ): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { @@ -569,11 +605,12 @@ export function connectHardware( ); } catch (error) { logErrorWithMessage(error); + const message = getErrorMessage(error); if ( deviceName === HardwareDeviceNames.ledger && ledgerTransportType === LedgerTransportTypes.webhid && isErrorWithMessage(error) && - error.message.match('Failed to open the device') + message.match('Failed to open the device') ) { dispatch(displayWarning(t('ledgerDeviceOpenFailureMessage'))); throw new Error(t('ledgerDeviceOpenFailureMessage')); @@ -1372,10 +1409,7 @@ export function cancelTx( return new Promise<void>((resolve, reject) => { callBackgroundMethod( 'rejectPendingApproval', - [ - String(txMeta.id), - ethErrors.provider.userRejectedRequest().serialize(), - ], + [String(txMeta.id), providerErrors.userRejectedRequest().serialize()], (error) => { if (error) { reject(error); @@ -1421,10 +1455,7 @@ export function cancelTxs( new Promise<void>((resolve, reject) => { callBackgroundMethod( 'rejectPendingApproval', - [ - String(id), - ethErrors.provider.userRejectedRequest().serialize(), - ], + [String(id), providerErrors.userRejectedRequest().serialize()], (err) => { if (err) { reject(err); @@ -1660,7 +1691,7 @@ export function lockMetamask(): ThunkAction< return backgroundSetLocked() .then(() => forceUpdateMetamaskState(dispatch)) .catch((error) => { - dispatch(displayWarning(error.message)); + dispatch(displayWarning(getErrorMessage(error))); return Promise.reject(error); }) .then(() => { @@ -1748,8 +1779,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 +1798,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 +1823,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 +1842,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, @@ -1992,15 +2084,17 @@ export function addNftVerifyOwnership( tokenID, ]); } catch (error) { - if ( - isErrorWithMessage(error) && - (error.message.includes('This NFT is not owned by the user') || - error.message.includes('Unable to verify ownership')) - ) { - throw error; - } else { - logErrorWithMessage(error); - dispatch(displayWarning(error)); + if (isErrorWithMessage(error)) { + const message = getErrorMessage(error); + if ( + message.includes('This NFT is not owned by the user') || + message.includes('Unable to verify ownership') + ) { + throw error; + } else { + logErrorWithMessage(error); + dispatch(displayWarning(error)); + } } } finally { await forceUpdateMetamaskState(dispatch); @@ -2552,6 +2646,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>) { @@ -2731,7 +2837,8 @@ export function displayWarning(payload: unknown): PayloadAction<string> { if (isErrorWithMessage(payload)) { return { type: actionConstants.DISPLAY_WARNING, - payload: payload.message, + payload: + (payload as DataWithOptionalCause)?.cause?.message || payload.message, }; } else if (typeof payload === 'string') { return { @@ -2923,6 +3030,7 @@ export function setFeatureFlag( export function setPreference( preference: string, value: boolean | string | object, + showLoading: boolan = true, ): ThunkAction< Promise<TemporaryPreferenceFlagDef>, MetaMaskReduxState, @@ -2930,13 +3038,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 +3066,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 +3078,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,13 +3114,16 @@ export function setRedesignedConfirmationsDeveloperEnabled(value: boolean) { return setPreference('isRedesignedConfirmationsDeveloperEnabled', value); } -export function setSmartTransactionsOptInStatus( +export function setTokenSortConfig(value: SortCriteria) { + return setPreference('tokenSortConfig', value, false); +} + +export function setSmartTransactionsPreferenceEnabled( value: boolean, ): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch, getState) => { - const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus( - getState(), - ); + const smartTransactionsOptInStatus = + getSmartTransactionsOptInStatusInternal(getState()); trackMetaMetricsEvent({ category: MetaMetricsEventCategory.Settings, event: MetaMetricsEventName.SettingsUpdated, @@ -3143,7 +3260,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 +3704,7 @@ export function fetchAndSetQuotes( fromAddress: string; balanceError: string; sourceDecimals: number; + enableGasIncludedQuotes: boolean; }, fetchParamsMetaData: { sourceTokenInfo: Token; @@ -3800,6 +3918,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. * @@ -3916,7 +4047,7 @@ export function resolvePendingApproval( // Before closing the current window, check if any additional confirmations // are added as a result of this confirmation being accepted - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) const { pendingApprovals } = await forceUpdateMetamaskState(_dispatch); if (Object.values(pendingApprovals).length === 0) { _dispatch(closeCurrentNotificationWindow()); @@ -3957,7 +4088,7 @@ export function rejectAllMessages( ): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { const userRejectionError = serializeError( - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ); await Promise.all( messageList.map( @@ -4110,7 +4241,7 @@ export function setConnectedStatusPopoverHasBeenShown(): ThunkAction< return () => { callBackgroundMethod('setConnectedStatusPopoverHasBeenShown', [], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; @@ -4120,7 +4251,7 @@ export function setRecoveryPhraseReminderHasBeenShown() { return () => { callBackgroundMethod('setRecoveryPhraseReminderHasBeenShown', [], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; @@ -4135,7 +4266,7 @@ export function setRecoveryPhraseReminderLastShown( [lastShown], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }, ); @@ -4160,6 +4291,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 +4516,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> { @@ -4606,12 +4751,15 @@ export function fetchSmartTransactionFees( return smartTransactionFees; } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -4667,18 +4815,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, }, ], @@ -4686,12 +4831,15 @@ export function signAndSendSmartTransaction({ return response.uuid; } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -4712,12 +4860,15 @@ export function updateSmartTransaction( ]); } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -4746,12 +4897,15 @@ export function cancelSmartTransaction( await submitRequestToBackground('cancelSmartTransaction', [uuid]); } catch (err) { logErrorWithMessage(err); - if (isErrorWithMessage(err) && err.message.startsWith('Fetch error:')) { - const errorObj = parseSmartTransactionsError(err.message); - dispatch({ - type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj, - }); + if (isErrorWithMessage(err)) { + const errorMessage = getErrorMessage(err); + if (errorMessage.startsWith('Fetch error:')) { + const errorObj = parseSmartTransactionsError(errorMessage); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: errorObj, + }); + } } throw err; } @@ -5362,6 +5516,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 +5739,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, @@ -5645,3 +5869,22 @@ function applyPatches( return newState; } + +export async function sendMultichainTransaction( + snapId: string, + account: string, + scope: string, +) { + await handleSnapRequest({ + snapId, + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + method: 'startSendTransactionFlow', + params: { + account, + scope, + }, + }, + }); +} diff --git a/ui/store/institutional/institution-background.test.js b/ui/store/institutional/institution-background.test.js index 7a3ca82e53eb..722bf7a3135c 100644 --- a/ui/store/institutional/institution-background.test.js +++ b/ui/store/institutional/institution-background.test.js @@ -11,6 +11,7 @@ import { setNoteToTraderMessage, setTypedMessageInProgress, setPersonalMessageInProgress, + logAndStoreApiRequest, } from './institution-background'; jest.mock('../actions', () => ({ @@ -173,4 +174,75 @@ describe('Institution Actions', () => { ); }); }); + + describe('#logAndStoreApiRequest', () => { + it('should call submitRequestToBackground with correct parameters', async () => { + const mockLogData = { + id: '123', + method: 'GET', + request: { + url: 'https://api.example.com/data', + headers: { 'Content-Type': 'application/json' }, + }, + response: { + status: 200, + body: '{"success": true}', + }, + timestamp: 1234567890, + }; + + await logAndStoreApiRequest(mockLogData); + + expect(submitRequestToBackground).toHaveBeenCalledWith( + 'logAndStoreApiRequest', + [mockLogData], + ); + }); + + it('should return the result from submitRequestToBackground', async () => { + const mockLogData = { + id: '456', + method: 'POST', + request: { + url: 'https://api.example.com/submit', + headers: { 'Content-Type': 'application/json' }, + body: '{"data": "test"}', + }, + response: { + status: 201, + body: '{"id": "789"}', + }, + timestamp: 1234567890, + }; + + submitRequestToBackground.mockResolvedValue('success'); + + const result = await logAndStoreApiRequest(mockLogData); + + expect(result).toBe('success'); + }); + + it('should throw an error if submitRequestToBackground fails', async () => { + const mockLogData = { + id: '789', + method: 'GET', + request: { + url: 'https://api.example.com/error', + headers: { 'Content-Type': 'application/json' }, + }, + response: { + status: 500, + body: '{"error": "Internal Server Error"}', + }, + timestamp: 1234567890, + }; + + const mockError = new Error('Background request failed'); + submitRequestToBackground.mockRejectedValue(mockError); + + await expect(logAndStoreApiRequest(mockLogData)).rejects.toThrow( + 'Background request failed', + ); + }); + }); }); diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts index 1604953f9b99..fd42d069b8b7 100644 --- a/ui/store/institutional/institution-background.ts +++ b/ui/store/institutional/institution-background.ts @@ -1,6 +1,7 @@ import log from 'loglevel'; import { ThunkAction } from 'redux-thunk'; import { AnyAction } from 'redux'; +import { IApiCallLogEntry } from '@metamask-institutional/types'; import { forceUpdateMetamaskState, displayWarning, @@ -12,7 +13,10 @@ import { submitRequestToBackground, } from '../background-connection'; import { MetaMaskReduxDispatch, MetaMaskReduxState } from '../store'; -import { isErrorWithMessage } from '../../../shared/modules/error'; +import { + isErrorWithMessage, + getErrorMessage, +} from '../../../shared/modules/error'; import { ConnectionRequest } from '../../../shared/constants/mmi-controller'; export function showInteractiveReplacementTokenBanner({ @@ -34,8 +38,8 @@ export function showInteractiveReplacementTokenBanner({ // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { if (err) { - dispatch(displayWarning(err.message)); - throw new Error(err.message); + dispatch(displayWarning(err)); + throw new Error(getErrorMessage(err)); } } }; @@ -80,7 +84,7 @@ export function setTypedMessageInProgress(msgId: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { log.error(error); - dispatch(displayWarning(error.message)); + dispatch(displayWarning(error)); } finally { await forceUpdateMetamaskState(dispatch); dispatch(hideLoadingIndication()); @@ -97,7 +101,7 @@ export function setPersonalMessageInProgress(msgId: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { log.error(error); - dispatch(displayWarning(error.message)); + dispatch(displayWarning(error)); } finally { await forceUpdateMetamaskState(dispatch); dispatch(hideLoadingIndication()); @@ -105,6 +109,12 @@ export function setPersonalMessageInProgress(msgId: string) { }; } +export async function logAndStoreApiRequest( + logData: IApiCallLogEntry, +): Promise<void> { + return await submitRequestToBackground('logAndStoreApiRequest', [logData]); +} + /** * A factory that contains all MMI actions ready to use * Example usage: @@ -135,7 +145,7 @@ export function mmiActionsFactory() { } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -157,7 +167,7 @@ export function mmiActionsFactory() { return () => { callBackgroundMethod(name, [payload], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; diff --git a/yarn.lock b/yarn.lock index 8e842fb9c927..af059f8960e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,7 +88,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.7": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.7": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" dependencies: @@ -177,24 +177,24 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.24.8, @babel/generator@npm:^7.7.2": - version: 7.24.10 - resolution: "@babel/generator@npm:7.24.10" +"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.25.4, @babel/generator@npm:^7.7.2": + version: 7.25.5 + resolution: "@babel/generator@npm:7.25.5" dependencies: - "@babel/types": "npm:^7.24.9" + "@babel/types": "npm:^7.25.4" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 10/c2491fb7d985527a165546cbcf9e5f6a2518f2a968c7564409c012acce1019056b21e67a152af89b3f4d4a295ca2e75a1a16858152f750efbc4b5087f0cb7253 + checksum: 10/e6d046afe739cfa706c40c127b7436731acb2a3146d408a7d89dbf16448491b35bc09b7d285cc19c2c1f8980d74b5a99df200d67c859bb5260986614685b0770 languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" +"@babel/helper-annotate-as-pure@npm:^7.22.5, @babel/helper-annotate-as-pure@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/53da330f1835c46f26b7bf4da31f7a496dee9fd8696cca12366b94ba19d97421ce519a74a837f687749318f94d1a37f8d1abcbf35e8ed22c32d16373b2f6198d + "@babel/types": "npm:^7.24.7" + checksum: 10/a9017bfc1c4e9f2225b967fbf818004703de7cf29686468b54002ffe8d6b56e0808afa20d636819fcf3a34b89ba72f52c11bdf1d69f303928ee10d92752cad95 languageName: node linkType: hard @@ -220,22 +220,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.5, @babel/helper-create-class-features-plugin@npm:^7.24.5": - version: 7.24.5 - resolution: "@babel/helper-create-class-features-plugin@npm:7.24.5" +"@babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.5, @babel/helper-create-class-features-plugin@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/helper-create-class-features-plugin@npm:7.25.4" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-member-expression-to-functions": "npm:^7.24.5" - "@babel/helper-optimise-call-expression": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.24.1" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.24.5" + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.8" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/helper-replace-supers": "npm:^7.25.0" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.4" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/9f65cf44ff838dae2a51ba7fdca1a27cc6eb7c0589e2446e807f7e8dc18e9866775f6e7a209d4f1d25bfed265e450ea338ca6c3570bc11a77fbfe683694130f3 + checksum: 10/47218da9fd964af30d41f0635d9e33eed7518e03aa8f10c3eb8a563bb2c14f52be3e3199db5912ae0e26058c23bb511c811e565c55ecec09427b04b867ed13c2 languageName: node linkType: hard @@ -267,7 +265,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5, @babel/helper-environment-visitor@npm:^7.24.7": +"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5": version: 7.24.7 resolution: "@babel/helper-environment-visitor@npm:7.24.7" dependencies: @@ -276,7 +274,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0, @babel/helper-function-name@npm:^7.24.7": +"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0": version: 7.24.7 resolution: "@babel/helper-function-name@npm:7.24.7" dependencies: @@ -286,7 +284,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.22.5, @babel/helper-hoist-variables@npm:^7.24.7": +"@babel/helper-hoist-variables@npm:^7.22.5": version: 7.24.7 resolution: "@babel/helper-hoist-variables@npm:7.24.7" dependencies: @@ -295,7 +293,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.23.0, @babel/helper-member-expression-to-functions@npm:^7.24.5": +"@babel/helper-member-expression-to-functions@npm:^7.24.8": version: 7.24.8 resolution: "@babel/helper-member-expression-to-functions@npm:7.24.8" dependencies: @@ -305,31 +303,31 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.24.3": - version: 7.24.3 - resolution: "@babel/helper-module-imports@npm:7.24.3" +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-module-imports@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.24.0" - checksum: 10/42fe124130b78eeb4bb6af8c094aa749712be0f4606f46716ce74bc18a5ea91c918c547c8bb2307a2e4b33f163e4ad2cb6a7b45f80448e624eae45b597ea3499 + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/df8bfb2bb18413aa151ecd63b7d5deb0eec102f924f9de6bc08022ced7ed8ca7fed914562d2f6fa5b59b74a5d6e255dc35612b2bc3b8abf361e13f61b3704770 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.23.0, @babel/helper-module-transforms@npm:^7.23.3": - version: 7.24.5 - resolution: "@babel/helper-module-transforms@npm:7.24.5" +"@babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.23.0, @babel/helper-module-transforms@npm:^7.24.8": + version: 7.25.2 + resolution: "@babel/helper-module-transforms@npm:7.25.2" dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-module-imports": "npm:^7.24.3" - "@babel/helper-simple-access": "npm:^7.24.5" - "@babel/helper-split-export-declaration": "npm:^7.24.5" - "@babel/helper-validator-identifier": "npm:^7.24.5" + "@babel/helper-module-imports": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.2" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/1a91e8abc2f427f8273ce3b99ef7b9c013eb3628221428553e0d4bc9c6db2e73bc4fc1b8535bd258544936accab9380e0d095f2449f913cad650ddee744b2124 + checksum: 10/a3bcf7815f3e9d8b205e0af4a8d92603d685868e45d119b621357e274996bf916216bb95ab5c6a60fde3775b91941555bf129d608e3d025b04f8aac84589f300 languageName: node linkType: hard -"@babel/helper-optimise-call-expression@npm:^7.22.5": +"@babel/helper-optimise-call-expression@npm:^7.22.5, @babel/helper-optimise-call-expression@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-optimise-call-expression@npm:7.24.7" dependencies: @@ -338,10 +336,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.24.5 - resolution: "@babel/helper-plugin-utils@npm:7.24.5" - checksum: 10/6e11ca5da73e6bd366848236568c311ac10e433fc2034a6fe6243af28419b07c93b4386f87bbc940aa058b7c83f370ef58f3b0fd598106be040d21a3d1c14276 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.24.8, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": + version: 7.24.8 + resolution: "@babel/helper-plugin-utils@npm:7.24.8" + checksum: 10/adbc9fc1142800a35a5eb0793296924ee8057fe35c61657774208670468a9fbfbb216f2d0bc46c680c5fefa785e5ff917cc1674b10bd75cdf9a6aa3444780630 languageName: node linkType: hard @@ -358,38 +356,40 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9, @babel/helper-replace-supers@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/helper-replace-supers@npm:7.24.1" +"@babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9, @babel/helper-replace-supers@npm:^7.25.0": + version: 7.25.0 + resolution: "@babel/helper-replace-supers@npm:7.25.0" dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-member-expression-to-functions": "npm:^7.23.0" - "@babel/helper-optimise-call-expression": "npm:^7.22.5" + "@babel/helper-member-expression-to-functions": "npm:^7.24.8" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.0" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/1103b28ce0cc7fba903c21bc78035c696ff191bdbbe83c20c37030a2e10ae6254924556d942cdf8c44c48ba606a8266fdb105e6bb10945de9285f79cb1905df1 + checksum: 10/97c6c17780cb9692132f7243f5a21fb6420104cb8ff8752dc03cfc9a1912a243994c0290c77ff096637ab6f2a7363b63811cfc68c2bad44e6b39460ac2f6a63f languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.22.5, @babel/helper-simple-access@npm:^7.24.5": - version: 7.24.5 - resolution: "@babel/helper-simple-access@npm:7.24.5" +"@babel/helper-simple-access@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-simple-access@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.24.5" - checksum: 10/db8768a16592faa1bde9061cac3d903bdbb2ddb2a7e9fb73c5904daee1f1b1dc69ba4d249dc22c45885c0d4b54fd0356ee78e6d67a9a90330c7dd37e6cd3acff + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/5083e190186028e48fc358a192e4b93ab320bd016103caffcfda81302a13300ccce46c9cd255ae520c25d2a6a9b47671f93e5fe5678954a2329dc0a685465c49 languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5" +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5, @babel/helper-skip-transparent-expression-wrappers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/1012ef2295eb12dc073f2b9edf3425661e9b8432a3387e62a8bc27c42963f1f216ab3124228015c748770b2257b4f1fda882ca8fa34c0bf485e929ae5bc45244 + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10/784a6fdd251a9a7e42ccd04aca087ecdab83eddc60fda76a2950e00eb239cc937d3c914266f0cc476298b52ac3f44ffd04c358e808bd17552a7e008d75494a77 languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.6, @babel/helper-split-export-declaration@npm:^7.24.5, @babel/helper-split-export-declaration@npm:^7.24.7": +"@babel/helper-split-export-declaration@npm:^7.22.6": version: 7.24.7 resolution: "@babel/helper-split-export-declaration@npm:7.24.7" dependencies: @@ -405,17 +405,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.24.5, @babel/helper-validator-identifier@npm:^7.24.7": +"@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-identifier@npm:7.24.7" checksum: 10/86875063f57361471b531dbc2ea10bbf5406e12b06d249b03827d361db4cad2388c6f00936bcd9dc86479f7e2c69ea21412c2228d4b3672588b754b70a449d4b languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/helper-validator-option@npm:7.23.5" - checksum: 10/537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e +"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5, @babel/helper-validator-option@npm:^7.24.7": + version: 7.24.8 + resolution: "@babel/helper-validator-option@npm:7.24.8" + checksum: 10/a52442dfa74be6719c0608fee3225bd0493c4057459f3014681ea1a4643cd38b68ff477fe867c4b356da7330d085f247f0724d300582fa4ab9a02efaf34d107c languageName: node linkType: hard @@ -482,12 +482,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.0, @babel/parser@npm:^7.13.9, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/parser@npm:7.24.8" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.0, @babel/parser@npm:^7.13.9, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.4": + version: 7.25.4 + resolution: "@babel/parser@npm:7.25.4" + dependencies: + "@babel/types": "npm:^7.25.4" bin: parser: ./bin/babel-parser.js - checksum: 10/e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef + checksum: 10/343b8a76c43549e370fe96f4f6d564382a6cdff60e9c3b8a594c51e4cefd58ec9945e82e8c4dfbf15ac865a04e4b29806531440760748e28568e6aec21bc9cb5 languageName: node linkType: hard @@ -645,14 +647,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.24.1, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.24.1 - resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.24.7, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.24.7 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.7" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-plugin-utils": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/712f7e7918cb679f106769f57cfab0bc99b311032665c428b98f4c3e2e6d567601d45386a4f246df6a80d741e1f94192b3f008800d66c4f1daae3ad825c243f0 + checksum: 10/a93516ae5b34868ab892a95315027d4e5e38e8bd1cfca6158f2974b0901cbb32bbe64ea10ad5b25f919ddc40c6d8113c4823372909c9c9922170c12b0b1acecb languageName: node linkType: hard @@ -744,14 +746,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.24.1, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.24.1 - resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" +"@babel/plugin-syntax-typescript@npm:^7.24.7, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.25.4 + resolution: "@babel/plugin-syntax-typescript@npm:7.25.4" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-plugin-utils": "npm:^7.24.8" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/bf4bd70788d5456b5f75572e47a2e31435c7c4e43609bd4dffd2cc0c7a6cf90aabcf6cd389e351854de9a64412a07d30effef5373251fe8f6a4c9db0c0163bda + checksum: 10/0771b45a35fd536cd3b3a48e5eda0f53e2d4f4a0ca07377cc247efa39eaf6002ed1c478106aad2650e54aefaebcb4f34f3284c4ae9252695dbd944bf66addfb0 languageName: node linkType: hard @@ -1047,16 +1049,16 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" +"@babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.7": + version: 7.24.8 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.8" dependencies: - "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.24.0" - "@babel/helper-simple-access": "npm:^7.22.5" + "@babel/helper-module-transforms": "npm:^7.24.8" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-simple-access": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7326a62ed5f766f93ee75684868635b59884e2801533207ea11561c296de53037949fecad4055d828fa7ebeb6cc9e55908aa3e7c13f930ded3e62ad9f24680d7 + checksum: 10/18e5d229767c7b5b6ff0cbf1a8d2d555965b90201839d0ac2dc043b56857624ea344e59f733f028142a8c1d54923b82e2a0185694ef36f988d797bfbaf59819c languageName: node linkType: hard @@ -1361,17 +1363,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.24.1": - version: 7.24.5 - resolution: "@babel/plugin-transform-typescript@npm:7.24.5" +"@babel/plugin-transform-typescript@npm:^7.24.7": + version: 7.25.2 + resolution: "@babel/plugin-transform-typescript@npm:7.25.2" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-create-class-features-plugin": "npm:^7.24.5" - "@babel/helper-plugin-utils": "npm:^7.24.5" - "@babel/plugin-syntax-typescript": "npm:^7.24.1" + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-create-class-features-plugin": "npm:^7.25.0" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/plugin-syntax-typescript": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/3d35accd6d7ae075509e01ce2cc3921ef3b44159b8ec15dd6201050c56dab4cfe14c5c0538e26e3beffb14c33731527041b60444cfba1ceae740f0748caf0aa0 + checksum: 10/50e017ffd131c08661daa22b6c759999bb7a6cdfbf683291ee4bcbea4ae839440b553d2f8896bcf049aca1d267b39f3b09e8336059e919e83149b5ad859671f6 languageName: node linkType: hard @@ -1554,18 +1557,18 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.23.2, @babel/preset-typescript@npm:^7.23.3": - version: 7.24.1 - resolution: "@babel/preset-typescript@npm:7.24.1" +"@babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.23.2, @babel/preset-typescript@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/preset-typescript@npm:7.24.7" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" - "@babel/helper-validator-option": "npm:^7.23.5" - "@babel/plugin-syntax-jsx": "npm:^7.24.1" - "@babel/plugin-transform-modules-commonjs": "npm:^7.24.1" - "@babel/plugin-transform-typescript": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/helper-validator-option": "npm:^7.24.7" + "@babel/plugin-syntax-jsx": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/ba774bd427c9f376769ddbc2723f5801a6b30113a7c3aaa14c36215508e347a527fdae98cfc294f0ecb283d800ee0c1f74e66e38e84c9bc9ed2fe6ed50dcfaf8 + checksum: 10/995e9783f8e474581e7533d6b10ec1fbea69528cc939ad8582b5937e13548e5215d25a8e2c845e7b351fdaa13139896b5e42ab3bde83918ea4e41773f10861ac languageName: node linkType: hard @@ -1610,12 +1613,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/runtime@npm:7.24.8" +"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.25.0": + version: 7.25.4 + resolution: "@babel/runtime@npm:7.25.4" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10/e6f335e472a8a337379effc15815dd0eddf6a7d0c00b50deb4f9e9585819b45431d0ff3c2d3d0fa58c227a9b04dcc4a85e7245fb57493adb2863b5208c769cbd + checksum: 10/70d2a420c24a3289ea6c4addaf3a1c4186bc3d001c92445faa3cd7601d7d2fbdb32c63b3a26b9771e20ff2f511fa76b726bf256f823cdb95bc37b8eadbd02f70 languageName: node linkType: hard @@ -1628,14 +1631,14 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.3.3": - version: 7.24.7 - resolution: "@babel/template@npm:7.24.7" +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3": + version: 7.25.0 + resolution: "@babel/template@npm:7.25.0" dependencies: "@babel/code-frame": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10/5975d404ef51cf379515eb0f80b115981d0b9dff5539e53a47516644abb8c83d7559f5b083eb1d4977b20d8359ebb2f911ccd4f729143f8958fdc465f976d843 + "@babel/parser": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" + checksum: 10/07ebecf6db8b28244b7397628e09c99e7a317b959b926d90455c7253c88df3677a5a32d1501d9749fe292a263ff51a4b6b5385bcabd5dadd3a48036f4d4949e0 languageName: node linkType: hard @@ -1657,21 +1660,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.12.5, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/traverse@npm:7.24.8" +"@babel/traverse@npm:^7.12.5, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.2, @babel/traverse@npm:^7.25.4": + version: 7.25.4 + resolution: "@babel/traverse@npm:7.25.4" dependencies: "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.24.8" - "@babel/helper-environment-visitor": "npm:^7.24.7" - "@babel/helper-function-name": "npm:^7.24.7" - "@babel/helper-hoist-variables": "npm:^7.24.7" - "@babel/helper-split-export-declaration": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.8" - "@babel/types": "npm:^7.24.8" + "@babel/generator": "npm:^7.25.4" + "@babel/parser": "npm:^7.25.4" + "@babel/template": "npm:^7.25.0" + "@babel/types": "npm:^7.25.4" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/47d8ecf8cfff58fe621fc4d8454b82c97c407816d8f9c435caa0c849ea7c357b91119a06f3c69f21a0228b5d06ac0b44f49d1f78cff032d6266317707f1fe615 + checksum: 10/a85c16047ab8e454e2e758c75c31994cec328bd6d8b4b22e915fa7393a03b3ab96d1218f43dc7ef77c957cc488dc38100bdf504d08a80a131e89b2e49cfa2be5 languageName: node linkType: hard @@ -1686,14 +1686,14 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.0, @babel/types@npm:^7.13.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.5, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.24.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.24.9 - resolution: "@babel/types@npm:7.24.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.0, @babel/types@npm:^7.13.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.25.4 + resolution: "@babel/types@npm:7.25.4" dependencies: "@babel/helper-string-parser": "npm:^7.24.8" "@babel/helper-validator-identifier": "npm:^7.24.7" to-fast-properties: "npm:^2.0.0" - checksum: 10/21873a08a124646824aa230de06af52149ab88206dca59849dcb3003990a6306ec2cdaa4147ec1127c0cfc5f133853cfc18f80d7f6337b6662a3c378ed565f15 + checksum: 10/d4a1194612d0a2a6ce9a0be325578b43d74e5f5278c67409468ba0a924341f0ad349ef0245ee8a36da3766efe5cc59cd6bb52547674150f97d8dc4c8cfa5d6b8 languageName: node linkType: hard @@ -2383,7 +2383,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/common@npm:^4.2.0, @ethereumjs/common@npm:^4.4.0": +"@ethereumjs/common@npm:^4.3.0, @ethereumjs/common@npm:^4.4.0": version: 4.4.0 resolution: "@ethereumjs/common@npm:4.4.0" dependencies: @@ -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: @@ -2441,7 +2441,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^5.1.0, @ethereumjs/tx@npm:^5.2.1": +"@ethereumjs/tx@npm:^5.1.0, @ethereumjs/tx@npm:^5.2.1, @ethereumjs/tx@npm:^5.3.0": version: 5.4.0 resolution: "@ethereumjs/tx@npm:5.4.0" dependencies: @@ -3997,15 +3997,16 @@ __metadata: languageName: node linkType: hard -"@jest/types@npm:^25.5.0": - version: 25.5.0 - resolution: "@jest/types@npm:25.5.0" +"@jest/types@npm:^26.6.2": + version: 26.6.2 + resolution: "@jest/types@npm:26.6.2" dependencies: "@types/istanbul-lib-coverage": "npm:^2.0.0" - "@types/istanbul-reports": "npm:^1.1.1" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" "@types/yargs": "npm:^15.0.0" - chalk: "npm:^3.0.0" - checksum: 10/49cb06ab867bb4085de86b1c86cd76983aa97179b5de65a1de6ee2f345563fc19543c1b7470d5b626f08190da4e3c2e66b6fd2091a3c4f7bc10be3a000db7f0f + chalk: "npm:^4.0.0" + checksum: 10/02d42749c8c6dc7e3184d0ff0293dd91c97233c2e6dc3708d61ef33d3162d4f07ad38d2d8a39abd94cf2fced69b92a87565c7099137c4529809242ca327254af languageName: node linkType: hard @@ -4627,60 +4628,60 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/custody-controller@npm:^0.2.30, @metamask-institutional/custody-controller@npm:^0.2.31": - version: 0.2.31 - resolution: "@metamask-institutional/custody-controller@npm:0.2.31" +"@metamask-institutional/custody-controller@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask-institutional/custody-controller@npm:0.3.0" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/f856c98db42a21639d9ec5d1c835bc302b5a1b3fb821aae8641f63a9400f8303b8fa578368a2f2d2a1ec0c148c070f809b8c0fa46fa3fd2fa29f80e0ec1da207 + checksum: 10/572e96d4b23566fb8dbf06ab0117c68c2d1db901deea69eee48d08f41ea3e1dbbbb3090c83cce6ff240ed8061e84df1b61befaf57da764b495eb0978d45fac42 languageName: node linkType: hard -"@metamask-institutional/custody-keyring@npm:^2.0.3": - version: 2.0.3 - resolution: "@metamask-institutional/custody-keyring@npm:2.0.3" +"@metamask-institutional/custody-keyring@npm:^2.1.0": + version: 2.1.0 + resolution: "@metamask-institutional/custody-keyring@npm:2.1.0" dependencies: "@ethereumjs/tx": "npm:^4.1.1" "@ethereumjs/util": "npm:^8.0.5" "@metamask-institutional/configuration-client": "npm:^2.0.1" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/obs-store": "npm:^9.0.0" crypto: "npm:^1.0.1" lodash.clonedeep: "npm:^4.5.0" - checksum: 10/987beeeed67fb92a436eb1318f48ec2cc0ceb1ae944b7f5b2e492dcdc28a4298c5a8d25a520022ac52f87a411f7341961100be47a9626fbb1674aed349d98737 + checksum: 10/78421e38fed4ad88412593a307fc13f220b0e5a83dee76de0032c835a7896bf23bb76030e4bb7d69bfa604db7a31faa6312ac64b05cc135d8afb723fb3660920 languageName: node linkType: hard -"@metamask-institutional/extension@npm:^0.3.27": - version: 0.3.27 - resolution: "@metamask-institutional/extension@npm:0.3.27" +"@metamask-institutional/extension@npm:^0.3.28": + version: 0.3.28 + resolution: "@metamask-institutional/extension@npm:0.3.28" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-controller": "npm:^0.2.30" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" + "@metamask-institutional/custody-controller": "npm:^0.3.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.1" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/transaction-update": "npm:^0.2.5" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/transaction-update": "npm:^0.2.6" + "@metamask-institutional/types": "npm:^1.2.0" jest-create-mock-instance: "npm:^2.0.0" jest-fetch-mock: "npm:3.0.3" lodash.clonedeep: "npm:^4.5.0" - checksum: 10/dc9eefe8045607cd415b9db4a8df833c9a523e9d06a3a0e49e4c6e85063924db1f117725a91c926f19ce26d0701fc175ea4ad38fb13a8a3b092434bcd7fd7882 + checksum: 10/a1f73c5281282ab1315ee19dd363330504300c036586ff64c98c176da8ac23046de8e8051956b4e15184faf0720bf324b81c406a1bf85295691c24f191b8f747 languageName: node linkType: hard -"@metamask-institutional/institutional-features@npm:^1.3.5": - version: 1.3.5 - resolution: "@metamask-institutional/institutional-features@npm:1.3.5" +"@metamask-institutional/institutional-features@npm:^1.3.6": + version: 1.3.6 + resolution: "@metamask-institutional/institutional-features@npm:1.3.6" dependencies: - "@metamask-institutional/custody-keyring": "npm:^2.0.3" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/1a154dbbfc71c9fee43d755d901423e3ea17ad149679225481fdc2d73ae95960e1805a792dbe660dd778703614ea5fd7390314bd7099c8ede510db1d23bc08ab + checksum: 10/a6b53f1b0ba8554595498153cbc0d32bb1a2d8374ad6ff9b617fea4e10872120000d14d9916b48ff9bafbac5da954ada99dca5f88f3ba21d4fbb80590804444c languageName: node linkType: hard @@ -4698,17 +4699,17 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/sdk@npm:^0.1.30": - version: 0.1.30 - resolution: "@metamask-institutional/sdk@npm:0.1.30" +"@metamask-institutional/sdk@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask-institutional/sdk@npm:0.2.0" dependencies: "@metamask-institutional/simplecache": "npm:^1.1.0" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/types": "npm:^1.2.0" "@types/jsonwebtoken": "npm:^9.0.1" - "@types/node": "npm:^20.11.17" + "@types/node": "npm:^20.14.9" bignumber.js: "npm:^9.1.1" jsonwebtoken: "npm:^9.0.0" - checksum: 10/3f36925fa9399a0ea06e2a64ea89accfb34f0a17581ab69652b4f325a948db10e88faebcca4f7c2d9f5f1f1c7f98bd8f970b7a489218dfd1be8cebc669a2f67e + checksum: 10/59f8b5eff176746ef3c9c406edda340ab04b37df1799d9b56e26fcede95441461d73d4be8b33f1dc3153cddea6baa876eba1232ca538da8f732a29801531a2f8 languageName: node linkType: hard @@ -4719,36 +4720,36 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/transaction-update@npm:^0.2.5": - version: 0.2.5 - resolution: "@metamask-institutional/transaction-update@npm:0.2.5" +"@metamask-institutional/transaction-update@npm:^0.2.6": + version: 0.2.6 + resolution: "@metamask-institutional/transaction-update@npm:0.2.6" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" - "@metamask-institutional/websocket-client": "npm:^0.2.5" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" + "@metamask-institutional/websocket-client": "npm:^0.2.6" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/9dbcf7c38a03becf61ab013f78df225da1f6de12976f328e7809c0edda5ab9e1aeee2b4d5b9430c15d5dc9f7040fa703c560c58073d601110895388c1c15d7a8 + checksum: 10/815c6faaaed9af25ed21d1339790e82622bef81f3c578269afde908dc95d36cc64a549c58164e24f20d9941e8c05e883d02c8886b741e50e3cf83960a8cb00d2 languageName: node linkType: hard -"@metamask-institutional/types@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask-institutional/types@npm:1.1.0" - checksum: 10/76f3c8529e4fe549bcabe60c39a66dd1a526aa7ea16fe7949e960a884d2c9e5e2e65db4d1123e23eeaae46f88b10aafe365cc693f5f632ef1a8e407373fe2fdf +"@metamask-institutional/types@npm:^1.2.0": + version: 1.2.0 + resolution: "@metamask-institutional/types@npm:1.2.0" + checksum: 10/3e28224c12f1ad955f114de919dbf4abbef19bd19cca3a4544898061d79518a94baa14121ebf6e5c6972dd6b1d1ec8071ebc50a77480ad944c26a2be53af5290 languageName: node linkType: hard -"@metamask-institutional/websocket-client@npm:^0.2.5": - version: 0.2.5 - resolution: "@metamask-institutional/websocket-client@npm:0.2.5" +"@metamask-institutional/websocket-client@npm:^0.2.6": + version: 0.2.6 + resolution: "@metamask-institutional/websocket-client@npm:0.2.6" dependencies: - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" mock-socket: "npm:^9.2.1" - checksum: 10/4743ccbb3a92a5b7ddccfd9f72741910bb93cc769023c8b9ee7944bb82f79938e45b10af5f7754b2898dc218c0e3874cb38aa628f96685fc69d956900723755d + checksum: 10/ba59b6d776fdc9d681ac0a294cd3eab961ba9d06d1ebd6a59fbe379cf640c421fdaaf53f6b6ab187ea3f1993b251292deb3c9d1fff8b6717fbd14f2512105190 languageName: node linkType: hard @@ -4762,18 +4763,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 +4801,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 +4819,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 +4862,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.3.0": + version: 38.3.0 + resolution: "@metamask/assets-controllers@npm:38.3.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4871,12 +4872,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 +4894,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/b6e69c9925c50f351b9de1e31cc5d9a4c0ab7cf1abf116c0669611ecb58b3890dd0de53d36bcaaea4f8c45d6ddc2c53eef80c42f93f8f303f1ee9d8df088872b + languageName: node + linkType: hard + +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch": + version: 38.3.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch::version=38.3.0&hash=e14ff8" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + "@ethersproject/address": "npm:^5.7.0" + "@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/1f57289a3a2a88f1f16e00a138b30b9a8e4ac894086732a463e6b47d5e984e0a7e05ef2ec345f0e1cd69857669253260d53d4c37b2b3d9b970999602fc01a21c languageName: node linkType: hard @@ -4943,10 +4982,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.8.1": + version: 0.8.1 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.8.1" + checksum: 10/0fff706a98c6f798ae0ae78bf9a8913c0b056b18aff64f994e521c5005ab7e326fafe1d383b2b7c248456948eaa263df3b31a081d620d82ed7c266857c94a955 languageName: node linkType: hard @@ -5329,21 +5368,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, @metamask/eth-snap-keyring@npm:^4.4.0": + version: 4.4.0 + resolution: "@metamask/eth-snap-keyring@npm:4.4.0" 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.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@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/fd9926ba3706506bd9a16d1c2501e7c6cd7b7e3e7ea332bc7f28e0fca1f67f4514da51e6f9f4541a7354a2363d04c09c445f61b98fdc366432e1def9c2f27d07 languageName: node linkType: hard @@ -5364,17 +5404,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 @@ -5542,6 +5582,17 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-engine@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/json-rpc-engine@npm:10.0.0" + dependencies: + "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^9.1.0" + checksum: 10/2c401a4a64392aeb11c4f7ca8d7b458ba1106cff1e0b3dba8b3e0cc90e82f8c55ac2dc9fdfcd914b289e3298fb726d637cf21382336dde2c207cf76129ce5eab + languageName: node + linkType: hard + "@metamask/json-rpc-engine@npm:^7.1.0, @metamask/json-rpc-engine@npm:^7.1.1, @metamask/json-rpc-engine@npm:^7.3.2": version: 7.3.3 resolution: "@metamask/json-rpc-engine@npm:7.3.3" @@ -5564,7 +5615,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2, @metamask/json-rpc-engine@npm:^9.0.3": +"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2": version: 9.0.3 resolution: "@metamask/json-rpc-engine@npm:9.0.3" dependencies: @@ -5575,18 +5626,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" @@ -5612,21 +5651,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: @@ -5642,7 +5667,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: @@ -5699,18 +5724,18 @@ __metadata: languageName: node linkType: hard -"@metamask/message-signing-snap@npm:^0.3.3": - version: 0.3.3 - resolution: "@metamask/message-signing-snap@npm:0.3.3" +"@metamask/message-signing-snap@npm:^0.4.0": + version: 0.4.0 + resolution: "@metamask/message-signing-snap@npm:0.4.0" dependencies: - "@metamask/rpc-errors": "npm:^6.2.1" - "@metamask/snaps-sdk": "npm:^3.1.1" - "@metamask/utils": "npm:^8.3.0" - "@noble/ciphers": "npm:^0.5.1" - "@noble/curves": "npm:^1.4.0" + "@metamask/rpc-errors": "npm:^6.3.0" + "@metamask/snaps-sdk": "npm:^6.0.0" + "@metamask/utils": "npm:^9.0.0" + "@noble/ciphers": "npm:^0.5.3" + "@noble/curves": "npm:^1.4.2" "@noble/hashes": "npm:^1.4.0" - zod: "npm:^3.22.4" - checksum: 10/8290f9779e826965082ef1c18189e96502a51b9ed3ade486dab91a1bcf4af150ffb04207f620ba2b98b7b268efe107d4953ab64fed0932b66b87c72f98cc944e + zod: "npm:^3.23.8" + checksum: 10/fb61da8f2999305f99ad5a1d6be2def224c88c1059fcdc8e70d06641d695eef82d9b8463c6b57d797a519aa70dc741b7cb59596f503faf2eff68a1647248b4de languageName: node linkType: hard @@ -5968,6 +5993,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" @@ -5985,22 +6026,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" @@ -6029,12 +6054,24 @@ __metadata: languageName: node linkType: hard -"@metamask/preinstalled-example-snap@npm:^0.1.0": - version: 0.1.0 - resolution: "@metamask/preinstalled-example-snap@npm:0.1.0" +"@metamask/preferences-controller@npm:^13.0.2": + version: 13.0.3 + resolution: "@metamask/preferences-controller@npm:13.0.3" dependencies: - "@metamask/snaps-sdk": "npm:^6.5.0" - checksum: 10/0540aa6c20b17171f3a3bcf9ea2a7be551d6abbf16de9bd55dce038c5602c62a3921c7e840b82a325b0db00f26b96f54568854bdcd091558bd3b8fa8c6188023 + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + peerDependencies: + "@metamask/keyring-controller": ^17.0.0 + checksum: 10/d922c2e603c7a1ef0301dcfc7d5b6aa0bbdd9c318f0857fbbc9e95606609ae806e69c46231288953ce443322039781404565a46fe42bdfa731c4f0da20448d32 + languageName: node + linkType: hard + +"@metamask/preinstalled-example-snap@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask/preinstalled-example-snap@npm:0.2.0" + dependencies: + "@metamask/snaps-sdk": "npm:^6.9.0" + checksum: 10/f8ad6f42c9bd7ce3b7fc9b45eecda6191320ff762b48c482ba4944a6d7a228682b833c15e56058f26ac7bb10417dfe9de340af1c8eb9bbe5dc03c665426ccb13 languageName: node linkType: hard @@ -6080,26 +6117,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" @@ -6149,13 +6166,23 @@ __metadata: languageName: node linkType: hard -"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1": - version: 6.3.1 - resolution: "@metamask/rpc-errors@npm:6.3.1" +"@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.0, @metamask/rpc-errors@npm:^6.3.1": + version: 6.4.0 + resolution: "@metamask/rpc-errors@npm:6.4.0" dependencies: "@metamask/utils": "npm:^9.0.0" fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f968fb490b13b632c2ad4770a144d67cecdff8d539cb8b489c732b08dab7a62fae65d7a2908ce8c5b77260317aa618948a52463f093fa8d9f84aee1c5f6f5daf + checksum: 10/9a17525aa8ce9ac142a94c04000dba7f0635e8e155c6c045f57eca36cc78c255318cca2fad4571719a427dfd2df64b70bc6442989523a8de555480668d666ad5 + languageName: node + linkType: hard + +"@metamask/rpc-errors@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/rpc-errors@npm:7.0.0" + dependencies: + "@metamask/utils": "npm:^9.0.0" + fast-safe-stringify: "npm:^2.0.6" + checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd languageName: node linkType: hard @@ -6183,35 +6210,37 @@ __metadata: languageName: node linkType: hard -"@metamask/selected-network-controller@npm:^18.0.1": - version: 18.0.1 - resolution: "@metamask/selected-network-controller@npm:18.0.1" +"@metamask/selected-network-controller@npm:^18.0.2": + version: 18.0.2 + resolution: "@metamask/selected-network-controller@npm:18.0.2" dependencies: "@metamask/base-controller": "npm:^7.0.1" - "@metamask/json-rpc-engine": "npm:^9.0.3" + "@metamask/json-rpc-engine": "npm:^10.0.0" "@metamask/swappable-obj-proxy": "npm:^2.2.0" "@metamask/utils": "npm:^9.1.0" peerDependencies: "@metamask/network-controller": ^21.0.0 "@metamask/permission-controller": ^11.0.0 - checksum: 10/79a862f352a819185a7bcc87f380a03bcc929db125467fa7e2ec0fc06647899b611a8cafe6aac14f2a02622f704b77e29cc833ab465b8c233eeb0a37b9a1dffc + checksum: 10/cf46a1a7d4ca19d6327aeb5918b2e904933b3ae6959184a2d5773be294d1b0dbe4d16189c46bfcbd83f33d95fe0c6e5cb64e4745fa0c75243db4c8304ab6ec8e 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:^20.0.0": + version: 20.0.0 + resolution: "@metamask/signature-controller@npm:20.0.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/eth-sig-util": "npm:^7.0.1" "@metamask/utils": "npm:^9.1.0" + jsonschema: "npm:^1.2.4" lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" peerDependencies: "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 "@metamask/logging-controller": ^6.0.0 - checksum: 10/9eec874bddee00a969a0231367c55c2b1768ad029c8125929603544ddc94b1e7c833457e39aa0aa5fed19608cb68633f0a90ca40a5639a8d6e2c84dbf9756feb + checksum: 10/5647e362b4478d9cdb9f04027d7bad950efbe310496fc0347a92649a084bb92fc92a7fc5f911f8835e0d6b4e7ed6cf572594a79a57a31240948b87dd2267cdf8 languageName: node linkType: hard @@ -6252,9 +6281,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.7.0": - version: 9.7.0 - resolution: "@metamask/snaps-controllers@npm:9.7.0" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.11.1": + version: 9.11.1 + resolution: "@metamask/snaps-controllers@npm:9.11.1" dependencies: "@metamask/approval-controller": "npm:^7.0.2" "@metamask/base-controller": "npm:^6.0.2" @@ -6266,9 +6295,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-rpc-methods": "npm:^11.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/utils": "npm:^9.2.1" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6281,30 +6310,30 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.7.1 + "@metamask/snaps-execution-environments": ^6.9.1 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/8a353819e60330ef3e338a40b1115d4c830b92b1cc0c92afb2b34bf46fbc906e6da5f905654e1d486cacd40b7025ec74d3cd01cb935090035ce9f1021ce5469f + checksum: 10/e9d47b62c39cf331d26a9e35dcf5c0452aff70980db31b42b56b11165d8d1dc7e3b5ad6b495644baa0276b18a7d9681bfb059388c4f2fb1b07c6bbc8b8da799b languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.7.2": - version: 6.7.2 - resolution: "@metamask/snaps-execution-environments@npm:6.7.2" +"@metamask/snaps-execution-environments@npm:^6.9.1": + version: 6.9.1 + resolution: "@metamask/snaps-execution-environments@npm:6.9.1" dependencies: "@metamask/json-rpc-engine": "npm:^9.0.2" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.8.0" + "@metamask/snaps-utils": "npm:^8.4.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/4b8ec4c0f6e628feeffd92fe4378fd204d2ed78012a1ed5282b24b00c78cebc3b6d7cb1306903b045a2ca887ecc0adafb2c96da4a19f2730a268f4912b36bec3 + checksum: 10/87fb63e89780ebeb9083c93988167e671ceb3d1c77980a2cd32801f83d285669859bfd248197d3a2d683119b87554f1f835965549ad04587c8c2fa2f01fa1f18 languageName: node linkType: hard @@ -6320,67 +6349,36 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.1.1": - version: 11.1.1 - resolution: "@metamask/snaps-rpc-methods@npm:11.1.1" +"@metamask/snaps-rpc-methods@npm:^11.5.0": + version: 11.5.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.5.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/permission-controller": "npm:^11.0.0" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" - checksum: 10/e23279dabc6f4ffe2c6c4a7003a624cd5e79b558d7981ec12c23e54a5da25cb7be9bc7bddfa8b2ce84af28a89b42076a2c14ab004b7a976a4426bf1e1de71b5b + checksum: 10/a89b79926d5204a70369cd70e5174290805e8f9ede8057a49e347bd0e680d88de40ddfc25b3e54f53a16c3080a736ab73b50ffe50623264564af13f8709a23d3 languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.5.1": - version: 6.5.1 - resolution: "@metamask/snaps-sdk@npm:6.5.1" +"@metamask/snaps-sdk@npm:^6.9.0": + version: 6.9.0 + resolution: "@metamask/snaps-sdk@npm:6.9.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - checksum: 10/7831fb2ca61a32ad43e971de9307b221f6bd2f65c84a3286f350cfdd2396166c58db6cd2fac9711654a211c8dc2049e591a79ab720b3f5ad562e434f75e95d32 + checksum: 10/ea2c34c4451f671acc6c3c0ad0d46e770e8b7d0741c1d78a30bc36b883f09a10e9a428b8b564ecd0171da95fdf78bb8ac0de261423a1b35de5d22852300a24ee languageName: node linkType: hard -"@metamask/snaps-utils@npm:8.1.1": - version: 8.1.1 - resolution: "@metamask/snaps-utils@npm:8.1.1" - dependencies: - "@babel/core": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/key-tree": "npm:^9.1.2" - "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/slip44": "npm:^4.0.0" - "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.1" - chalk: "npm:^4.1.2" - cron-parser: "npm:^4.5.0" - fast-deep-equal: "npm:^3.1.3" - fast-json-stable-stringify: "npm:^2.1.0" - fast-xml-parser: "npm:^4.4.1" - marked: "npm:^12.0.1" - rfdc: "npm:^1.3.0" - semver: "npm:^7.5.4" - ses: "npm:^1.1.0" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f - languageName: node - linkType: hard - -"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.0": +"@metamask/snaps-utils@npm:^7.4.0": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" dependencies: @@ -6411,9 +6409,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch": - version: 8.1.1 - resolution: "@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch::version=8.1.1&hash=d09097" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1": + version: 8.4.1 + resolution: "@metamask/snaps-utils@npm:8.4.1" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6423,7 +6421,7 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" @@ -6438,7 +6436,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/6b1d3d70c5ebee684d5b76bf911c66ebd122a0607cefcfc9fffd4bf6882a7acfca655d97be87c0f7f47e59a981b58234578ed8a123e554a36e6c48ff87492655 + checksum: 10/c68a2fe69dc835c2b996d621fd4698435475d419a85aa557aa000aae0ab7ebb68d2a52f0b28bbab94fff895ece9a94077e3910a21b16d904cff3b9419ca575b6 languageName: node linkType: hard @@ -6479,10 +6477,10 @@ __metadata: languageName: node linkType: hard -"@metamask/test-dapp@npm:^8.4.0": - version: 8.4.0 - resolution: "@metamask/test-dapp@npm:8.4.0" - checksum: 10/9d9c4df11c2b18c72b52e8743435ed0bd18815dd7a7aed43cf3a2cce1b9ef8926909890d00b4b624446f73b88c15e95bc0190c5437b9dad437a0e345a6b430ba +"@metamask/test-dapp@npm:8.7.0": + version: 8.7.0 + resolution: "@metamask/test-dapp@npm:8.7.0" + checksum: 10/c2559179d3372e5fc8d67a60c1e4056fad9809486eaff6a2aa9c351a2a613eeecc15885a5fd9b71b8f4139058fe168abeac06bd6bdb6d4a47fe0b9b4146923ab languageName: node linkType: hard @@ -6523,9 +6521,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" @@ -6552,7 +6550,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 @@ -6603,9 +6601,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" @@ -6616,7 +6614,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 @@ -6669,7 +6667,7 @@ __metadata: languageName: node linkType: hard -"@noble/ciphers@npm:^0.5.1, @noble/ciphers@npm:^0.5.2": +"@noble/ciphers@npm:^0.5.2, @noble/ciphers@npm:^0.5.3": version: 0.5.3 resolution: "@noble/ciphers@npm:0.5.3" checksum: 10/af0ad96b5807feace93e63549e05de6f5e305b36e2e95f02d90532893fbc3af3f19b9621b6de4caa98303659e5df2e7aa082064e5d4a82e6f38c728d48dfae5d @@ -6685,7 +6683,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.2.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": +"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.2.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": version: 1.4.2 resolution: "@noble/curves@npm:1.4.2" dependencies: @@ -7846,6 +7844,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" @@ -8132,11 +8137,11 @@ __metadata: languageName: node linkType: hard -"@solana/web3.js@npm:^1.90.2, @solana/web3.js@npm:^1.91.6": - version: 1.95.1 - resolution: "@solana/web3.js@npm:1.95.1" +"@solana/web3.js@npm:^1.95.0": + version: 1.95.3 + resolution: "@solana/web3.js@npm:1.95.3" dependencies: - "@babel/runtime": "npm:^7.24.8" + "@babel/runtime": "npm:^7.25.0" "@noble/curves": "npm:^1.4.2" "@noble/hashes": "npm:^1.4.0" "@solana/buffer-layout": "npm:^4.0.1" @@ -8151,7 +8156,7 @@ __metadata: node-fetch: "npm:^2.7.0" rpc-websockets: "npm:^9.0.2" superstruct: "npm:^2.0.2" - checksum: 10/6b9b00bba37cf8b1f5de9b1bc82efc2006eb2fa8fd5b90bee6f0ce174c0a1a41e97e5ee1db8391fc8a1d50b4610a77744cb3b1364584a3d65bc931a26d635193 + checksum: 10/25bdc5100faae6d3e48cbfac965b129060bec61669dcd75d0a525cea3ce8d23632ebea249a7b21616c89641bf7ea26d18826ce51246274b6aa1278d32180c870 languageName: node linkType: hard @@ -9512,16 +9517,19 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^7.17.1": - version: 7.22.2 - resolution: "@testing-library/dom@npm:7.22.2" +"@testing-library/dom@npm:^7.17.1, @testing-library/dom@npm:^7.31.2": + version: 7.31.2 + resolution: "@testing-library/dom@npm:7.31.2" dependencies: - "@babel/runtime": "npm:^7.10.3" + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" "@types/aria-query": "npm:^4.2.0" aria-query: "npm:^4.2.2" - dom-accessibility-api: "npm:^0.5.0" - pretty-format: "npm:^25.5.0" - checksum: 10/2da0d8d577be7d5cfb6cf2b712e4ca65671e090190eb3ffdebd336c5ef2158dac4dee12709c6e06a38810291c7f407701187e7eec86f0b5ad2ff76487d28382d + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.6" + lz-string: "npm:^1.4.4" + pretty-format: "npm:^26.6.2" + checksum: 10/5082aaf14c80df529738d4ee3e85170371236162ce908430516ab6c9c581ea31e9ac9b87fdc9a8d298f98956c683b2068b029fcfdb5785ab7247348a6eab3854 languageName: node linkType: hard @@ -9606,87 +9614,87 @@ __metadata: languageName: node linkType: hard -"@trezor/analytics@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/analytics@npm:1.1.0" +"@trezor/analytics@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/analytics@npm:1.2.0" dependencies: - "@trezor/env-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/env-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/6a5b426c12b7ba7bfbbb955ac003733ca0b36a33f52d49c13a37ab341ae6f9c38a5aa0696f60dd31da650b01326a93d27d06ef830190a608159cc833451a413b + checksum: 10/652dea1b54515c10931fe67671a5043b22557629224da3ae8fff153a4a9af45eb27c7cc2cdef68e0dbfab53b7544df0dce1a903adf4e0c0c27531a6abc1d2a19 languageName: node linkType: hard -"@trezor/blockchain-link-types@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/blockchain-link-types@npm:1.1.0" +"@trezor/blockchain-link-types@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/blockchain-link-types@npm:1.2.0" dependencies: - "@solana/web3.js": "npm:^1.91.6" + "@solana/web3.js": "npm:^1.95.0" "@trezor/type-utils": "npm:1.1.0" - "@trezor/utxo-lib": "npm:2.1.0" + "@trezor/utxo-lib": "npm:2.2.0" socks-proxy-agent: "npm:6.1.1" peerDependencies: tslib: ^2.6.2 - checksum: 10/d7730bf1cc9e77293d5bf4dc7138d0719f0ae564273b51b1f142dc527269147e7701d8a20dc5f96326cb0a7d8294eb4394d6a0076ef692c78763bcb10633b62d + checksum: 10/3165250e4404ed8f4619662aa9a3aca0057da8867a8919a8b4a44b2643bda29661e65224946b3e5ab2c8e13677308f87dc0cdfaaa9324da886132fbe1899b841 languageName: node linkType: hard -"@trezor/blockchain-link-utils@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/blockchain-link-utils@npm:1.1.0" +"@trezor/blockchain-link-utils@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/blockchain-link-utils@npm:1.2.0" dependencies: "@mobily/ts-belt": "npm:^3.13.1" - "@solana/web3.js": "npm:^1.91.6" - "@trezor/env-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@solana/web3.js": "npm:^1.95.0" + "@trezor/env-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/b7866a4a59afc76b60dcd785ad9567cbfae4810a7510b09a7f1e2b0207834677216ce5b9c82c6f9a228cfb51e38ecf4f7e0f7501c67c5f083a4db06a8a9786e5 + checksum: 10/dacc6529ac568901269478484436b99da8e54dd111fc1663ff66a1b71e799d3663a21df1ce232acc11d4776eb9c77b976e52eafb3b496d941b5ad0996cc6b027 languageName: node linkType: hard -"@trezor/blockchain-link@npm:2.2.0": - version: 2.2.0 - resolution: "@trezor/blockchain-link@npm:2.2.0" +"@trezor/blockchain-link@npm:2.3.0": + version: 2.3.0 + resolution: "@trezor/blockchain-link@npm:2.3.0" dependencies: "@solana/buffer-layout": "npm:^4.0.1" - "@solana/web3.js": "npm:^1.90.2" - "@trezor/blockchain-link-types": "npm:1.1.0" - "@trezor/blockchain-link-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" - "@trezor/utxo-lib": "npm:2.1.0" + "@solana/web3.js": "npm:^1.95.0" + "@trezor/blockchain-link-types": "npm:1.2.0" + "@trezor/blockchain-link-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" + "@trezor/utxo-lib": "npm:2.2.0" "@types/web": "npm:^0.0.138" events: "npm:^3.3.0" ripple-lib: "npm:^1.10.1" socks-proxy-agent: "npm:6.1.1" - ws: "npm:^8.17.1" + ws: "npm:^8.18.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/3e0c5ddadb6d66f9c1b87ebecf9ac35729f1929c840bcab512de71cae04cf04d4a3562e6893443e57143adbf4a66de5780e29e95e969103a756bdb9454988313 + checksum: 10/46358539986f4804a2a9de51f01ca0cf8cf0183ec70bef2d2bce6d7baa813a9b6220657c39cbf5a0a1e5e27db0670f1cfbcaae8eb804bdd6d2327d6a798e7068 languageName: node linkType: hard -"@trezor/connect-analytics@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/connect-analytics@npm:1.1.0" +"@trezor/connect-analytics@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/connect-analytics@npm:1.2.0" dependencies: - "@trezor/analytics": "npm:1.1.0" + "@trezor/analytics": "npm:1.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/e6beecb036be00d3c62af7f4f4ff96a6756df698ac19807a1b4be3fb0bd50a702780ee9a47e7e64ffebfab353ee532b07d0b5e7efdb3b611f88b9d8f9bb40157 + checksum: 10/15763dc7ddd3c8b8033c9e14cce2104639b47b1e5c4f1faabe61d4275ad2ab00368216949d1085d17b6ba1c106ab2ee3627a0afb4923152e71eb9f92db5c4459 languageName: node linkType: hard -"@trezor/connect-common@npm:0.1.0": - version: 0.1.0 - resolution: "@trezor/connect-common@npm:0.1.0" +"@trezor/connect-common@npm:0.2.0": + version: 0.2.0 + resolution: "@trezor/connect-common@npm:0.2.0" dependencies: - "@trezor/env-utils": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/env-utils": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/a88a2798597bfa876876ad98752fd76dbad6c185f0130127e032279a21c78b1dc3af93a5a7b8e29956acf84645f65843964a5ec2c8ef4dc30cc6cfe8f6507d45 + checksum: 10/54313304deabed9349b2cd147613dfdbfdee32ddac5a111c077b5991eb5d123cc65f28f81c9049f27d9601d610d7f3c6df3374315695a90691a0d84bf9a4b43e languageName: node linkType: hard @@ -9700,63 +9708,50 @@ __metadata: languageName: node linkType: hard -"@trezor/connect-web@npm:9.3.0": - version: 9.3.0 - resolution: "@trezor/connect-web@npm:9.3.0" +"@trezor/connect-web@npm:^9.1.11, @trezor/connect-web@npm:^9.4.0": + version: 9.4.0 + resolution: "@trezor/connect-web@npm:9.4.0" dependencies: - "@trezor/connect": "npm:9.3.0" - "@trezor/connect-common": "npm:0.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/connect": "npm:9.4.0" + "@trezor/connect-common": "npm:0.2.0" + "@trezor/utils": "npm:9.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/a09a04f33d44ea2934863650313dda1f255e8e0ce283760e0e9bccbec49234865b9620cf37d2e4e4f5426b32f62bd3e2618f24df6df16248c159baf2fdb1eb0e + checksum: 10/16bf476da1a0800d062379cda7b9fc06f0d296cd268d2c8995c0b2d4db37dd24668fd440543aded5f9737ff92c5defa4c0f854332d128ff31d4141430d92dc75 languageName: node linkType: hard -"@trezor/connect-web@patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch": - version: 9.3.0 - resolution: "@trezor/connect-web@patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch::version=9.3.0&hash=3ffb2f" +"@trezor/connect@npm:9.4.0": + version: 9.4.0 + resolution: "@trezor/connect@npm:9.4.0" dependencies: - "@trezor/connect": "npm:9.3.0" - "@trezor/connect-common": "npm:0.1.0" - "@trezor/utils": "npm:9.1.0" - peerDependencies: - tslib: ^2.6.2 - checksum: 10/58781efa397d2028c0eb6362fb1a8567e0b8c4ab22581633742a20f6ece37e0011e7005a951956dfee3cf3360d6e72f2c7a82549611284897631d39959ccb37f - languageName: node - linkType: hard - -"@trezor/connect@npm:9.3.0": - version: 9.3.0 - resolution: "@trezor/connect@npm:9.3.0" - dependencies: - "@babel/preset-typescript": "npm:^7.23.3" - "@ethereumjs/common": "npm:^4.2.0" - "@ethereumjs/tx": "npm:^5.2.1" + "@babel/preset-typescript": "npm:^7.24.7" + "@ethereumjs/common": "npm:^4.3.0" + "@ethereumjs/tx": "npm:^5.3.0" "@fivebinaries/coin-selection": "npm:2.2.1" - "@trezor/blockchain-link": "npm:2.2.0" - "@trezor/blockchain-link-types": "npm:1.1.0" - "@trezor/connect-analytics": "npm:1.1.0" - "@trezor/connect-common": "npm:0.1.0" - "@trezor/protobuf": "npm:1.1.0" - "@trezor/protocol": "npm:1.1.0" - "@trezor/schema-utils": "npm:1.1.0" - "@trezor/transport": "npm:1.2.0" - "@trezor/utils": "npm:9.1.0" - "@trezor/utxo-lib": "npm:2.1.0" + "@trezor/blockchain-link": "npm:2.3.0" + "@trezor/blockchain-link-types": "npm:1.2.0" + "@trezor/connect-analytics": "npm:1.2.0" + "@trezor/connect-common": "npm:0.2.0" + "@trezor/protobuf": "npm:1.2.0" + "@trezor/protocol": "npm:1.2.0" + "@trezor/schema-utils": "npm:1.2.0" + "@trezor/transport": "npm:1.3.0" + "@trezor/utils": "npm:9.2.0" + "@trezor/utxo-lib": "npm:2.2.0" blakejs: "npm:^1.2.1" bs58: "npm:^5.0.0" bs58check: "npm:^3.0.1" cross-fetch: "npm:^4.0.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/4210e5c72d59a3bc3a40597b585aa425967865e5067b51714157f239184ca23f9044e145948dfd4afb2aa0a7405563c8286f0bfe2ef1b9cd947e63eee283f962 + checksum: 10/1f1e0dd077474643a908acd2e9089cf62202202e377b4171a5f5c03ddb5f8c5bae8694d113cb8bc047af4d79305b62f60c342af80bd7f51c7fe0c6e18a7ba9b1 languageName: node linkType: hard -"@trezor/env-utils@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/env-utils@npm:1.1.0" +"@trezor/env-utils@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/env-utils@npm:1.2.0" dependencies: ua-parser-js: "npm:^1.0.37" peerDependencies: @@ -9771,58 +9766,57 @@ __metadata: optional: true react-native: optional: true - checksum: 10/1b09c9ebc6070396528d5f1f9f44085b0465356cfcb936a7d69cff0b26ee024d90f0bf4e531cc927a5744651d70d3fddbd4d8e5aa771a9b62b86c29d08d2682d + checksum: 10/8b63897816ceb4437847f8672bb2767394addfae47964e5435c417600b8e3b24388d1d928c30e3acccf84547508f330829db7adb517008225da76dbd3c403a19 languageName: node linkType: hard -"@trezor/protobuf@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/protobuf@npm:1.1.0" +"@trezor/protobuf@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/protobuf@npm:1.2.0" dependencies: - "@trezor/schema-utils": "npm:1.1.0" + "@trezor/schema-utils": "npm:1.2.0" protobufjs: "npm:7.2.6" peerDependencies: tslib: ^2.6.2 - checksum: 10/61846d9a236af832834a7d6c3a8f73e81f83effe9c66a37cab6819f4318e019b63749aa450c2c8bf3b24ed745f3897d01dd4b23b144a43c62a6f2359055b8710 + checksum: 10/1f510e384b0e7d1a60ecc1dd05be14a8071834138e8bb64593a8585eff81298680d055c06ec3aa11133fa08b0283630ed0fa9301165f1765ed3d6d56e207835f languageName: node linkType: hard -"@trezor/protocol@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/protocol@npm:1.1.0" +"@trezor/protocol@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/protocol@npm:1.2.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/860601a91621561d8e8b5c4004d3d6f6ef5ab34a2c793ce9554ff0989d4a8f57465f5f1d93a8c3f828366449254d8357efa661770d2ed135d70a88de6b7d36c8 + checksum: 10/4440973bc20cc3f58c489f7a90292591c8994bace7477205287b504947d0a1e4ea7bf9e029e6a6bdd438281a8d9ff7ea54567dc377b39b8eaa7028522d12adca languageName: node linkType: hard -"@trezor/schema-utils@npm:1.1.0": - version: 1.1.0 - resolution: "@trezor/schema-utils@npm:1.1.0" +"@trezor/schema-utils@npm:1.2.0": + version: 1.2.0 + resolution: "@trezor/schema-utils@npm:1.2.0" dependencies: "@sinclair/typebox": "npm:^0.31.28" ts-mixer: "npm:^6.0.3" peerDependencies: tslib: ^2.6.2 - checksum: 10/cb0d6fa877f44b10d41b4d5f07e5852776da16b1fb76395f35d3a310701c809bc68f9ffa9c13487a9fcdbbabf0edafe70193b1bedc43329267885857eabaa5e7 + checksum: 10/ce1e4c8d95068e45834d33346d3596745e9263d3ac58482a56010584dfd89383e3915dee9f2b729ee411a2b417c3b4e14575192e462e576630124f9ea3957d28 languageName: node linkType: hard -"@trezor/transport@npm:1.2.0": - version: 1.2.0 - resolution: "@trezor/transport@npm:1.2.0" +"@trezor/transport@npm:1.3.0": + version: 1.3.0 + resolution: "@trezor/transport@npm:1.3.0" dependencies: - "@trezor/protobuf": "npm:1.1.0" - "@trezor/protocol": "npm:1.1.0" - "@trezor/utils": "npm:9.1.0" + "@trezor/protobuf": "npm:1.2.0" + "@trezor/protocol": "npm:1.2.0" + "@trezor/utils": "npm:9.2.0" cross-fetch: "npm:^4.0.0" - json-stable-stringify: "npm:^1.1.1" long: "npm:^4.0.0" protobufjs: "npm:7.2.6" usb: "npm:^2.11.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/e3725f98d5fa35956c81d2f9f0cf64149d7747195842654572e40810da94c1e44c8ca021fa55495d3c547f19c130f28fd13b13c051643fecb3c395c01428fc7b + checksum: 10/0b345bf848fddcf46c8c44e1f5c659794ab4a790749522fe266e1f81f6a612a477cef99c1104505aff39e976e1a25a868249ef440322faa76cea1cf8a02ffc78 languageName: node linkType: hard @@ -9833,22 +9827,22 @@ __metadata: languageName: node linkType: hard -"@trezor/utils@npm:9.1.0": - version: 9.1.0 - resolution: "@trezor/utils@npm:9.1.0" +"@trezor/utils@npm:9.2.0": + version: 9.2.0 + resolution: "@trezor/utils@npm:9.2.0" dependencies: bignumber.js: "npm:^9.1.2" peerDependencies: tslib: ^2.6.2 - checksum: 10/59590dcbb7c062991cbe0075a1b5e3b683929f2251ade96f90da12b2a01accbe14a12ef8d52e028934c97466aaeeb971b82669f0ecc69c52c42eb25f68ba92b3 + checksum: 10/9ca9f47af18cf939d02b2481666d0af15d58e53dabcae59fb9e5c18d65edcc91f793cf9104bf6505ba3041d8d2b8c9d61e252df2d5cb8e665e8b7ac41c3ac4c7 languageName: node linkType: hard -"@trezor/utxo-lib@npm:2.1.0": - version: 2.1.0 - resolution: "@trezor/utxo-lib@npm:2.1.0" +"@trezor/utxo-lib@npm:2.2.0": + version: 2.2.0 + resolution: "@trezor/utxo-lib@npm:2.2.0" dependencies: - "@trezor/utils": "npm:9.1.0" + "@trezor/utils": "npm:9.2.0" bchaddrjs: "npm:^0.5.2" bech32: "npm:^2.0.0" bip66: "npm:^1.1.5" @@ -9867,7 +9861,7 @@ __metadata: wif: "npm:^4.0.0" peerDependencies: tslib: ^2.6.2 - checksum: 10/6b57d393c0315e8599a2381b6f09f6df419e8d11e068dd853d6c8556c113fdd6167d696507a5e32e990e09ad3b84645874e15773f83b78e3df7b7bfe040125d3 + checksum: 10/398f58ca12efb4cc72985bd8bd6a9b637a49d0c56f4de8a7eb0332c7fa7e1e797a96a103dd55fed44cc0ed630c51e7d8712b17895ac26347087c4ffd5a5a456e languageName: node linkType: hard @@ -10505,16 +10499,6 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-reports@npm:^1.1.1": - version: 1.1.2 - resolution: "@types/istanbul-reports@npm:1.1.2" - dependencies: - "@types/istanbul-lib-coverage": "npm:*" - "@types/istanbul-lib-report": "npm:*" - checksum: 10/00866e815d1e68d0a590d691506937b79d8d65ad8eab5ed34dbfee66136c7c0f4ea65327d32046d5fe469f22abea2b294987591dc66365ebc3991f7e413b2d78 - languageName: node - linkType: hard - "@types/istanbul-reports@npm:^3.0.0": version: 3.0.0 resolution: "@types/istanbul-reports@npm:3.0.0" @@ -10707,12 +10691,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.11.17": - version: 20.12.7 - resolution: "@types/node@npm:20.12.7" +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.14.9": + version: 20.16.11 + resolution: "@types/node@npm:20.16.11" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/b4a28a3b593a9bdca5650880b6a9acef46911d58cf7cfa57268f048e9a7157a7c3196421b96cea576850ddb732e3b54bc982c8eb5e1e5ef0635d4424c2fce801 + undici-types: "npm:~6.19.2" + checksum: 10/6d2f92b7b320c32ba0c2bc54d21651bd21690998a2e27f00d15019d4db3e0ec30fce85332efed5e37d4cda078ff93ea86ee3e92b76b7a25a9b92a52a039b60b2 languageName: node linkType: hard @@ -13317,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" @@ -13501,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" @@ -14394,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 @@ -16867,10 +16869,10 @@ __metadata: languageName: node linkType: hard -"dom-accessibility-api@npm:^0.5.0": - version: 0.5.0 - resolution: "dom-accessibility-api@npm:0.5.0" - checksum: 10/2448657f072b4664f69616788da03f2f76ed5a47e21b8d36e872240eb9a3ca638c2f09fb9a31d9055ded4b50b0ef3013831dca47db62b9f809cb67ec9050bcd1 +"dom-accessibility-api@npm:^0.5.6": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16" + checksum: 10/377b4a7f9eae0a5d72e1068c369c99e0e4ca17fdfd5219f3abd32a73a590749a267475a59d7b03a891f9b673c27429133a818c44b2e47e32fec024b34274e2ca languageName: node linkType: hard @@ -18421,6 +18423,13 @@ __metadata: languageName: node linkType: hard +"eth-chainlist@npm:~0.0.498": + version: 0.0.498 + resolution: "eth-chainlist@npm:0.0.498" + checksum: 10/a414c0e1f0a877f9ab8bf1cf775556308ddbb66618e368666d4dea9a0b949febedf8ca5440cf57419413404e7661f1e3d040802faf532d0e1618c40ecd334cbf + languageName: node + linkType: hard + "eth-eip712-util-browser@npm:^0.0.3": version: 0.0.3 resolution: "eth-eip712-util-browser@npm:0.0.3" @@ -18477,15 +18486,6 @@ __metadata: languageName: node linkType: hard -"eth-rpc-errors@npm:^4.0.2": - version: 4.0.3 - resolution: "eth-rpc-errors@npm:4.0.3" - dependencies: - fast-safe-stringify: "npm:^2.0.6" - checksum: 10/47ce14170eabaee51ab1cc7e643bb3ef96ee6b15c6404806aedcd51750e00ae0b1a12c37785b180679b8d452b6dd44a0240bb018d01fa73efc85fcfa808b35a7 - languageName: node - linkType: hard - "ethereum-cryptography@npm:^0.1.3": version: 0.1.3 resolution: "ethereum-cryptography@npm:0.1.3" @@ -20813,13 +20813,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" @@ -21310,12 +21303,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 @@ -24184,16 +24177,6 @@ __metadata: languageName: node linkType: hard -"json-rpc-engine@npm:^6.1.0": - version: 6.1.0 - resolution: "json-rpc-engine@npm:6.1.0" - dependencies: - "@metamask/safe-event-emitter": "npm:^2.0.0" - eth-rpc-errors: "npm:^4.0.2" - checksum: 10/00d5b5228e90f126dd52176598db6e5611d295d3a3f7be21254c30c1b6555811260ef2ec2df035cd8e583e4b12096259da721e29f4ea2affb615f7dfc960a6a6 - languageName: node - linkType: hard - "json-rpc-middleware-stream@npm:^5.0.1": version: 5.0.1 resolution: "json-rpc-middleware-stream@npm:5.0.1" @@ -24276,7 +24259,7 @@ __metadata: languageName: node linkType: hard -"json-stable-stringify@npm:1.1.1, json-stable-stringify@npm:^1.0.0, json-stable-stringify@npm:^1.1.1": +"json-stable-stringify@npm:1.1.1, json-stable-stringify@npm:^1.0.0": version: 1.1.1 resolution: "json-stable-stringify@npm:1.1.1" dependencies: @@ -25363,6 +25346,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" @@ -25452,6 +25442,15 @@ __metadata: languageName: node linkType: hard +"lz-string@npm:^1.4.4": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 10/e86f0280e99a8d8cd4eef24d8601ddae15ce54e43ac9990dfcb79e1e081c255ad24424a30d78d2ad8e51a8ce82a66a930047fed4b4aa38c6f0b392ff9300edfc + languageName: node + linkType: hard + "magic-string@npm:^0.25.7": version: 0.25.7 resolution: "magic-string@npm:0.25.7" @@ -26069,6 +26068,7 @@ __metadata: "@ethersproject/providers": "npm:^5.7.2" "@ethersproject/wallet": "npm:^5.7.0" "@fortawesome/fontawesome-free": "npm:^5.13.0" + "@jest/globals": "npm:^29.7.0" "@keystonehq/bc-ur-registry-eth": "npm:^0.19.1" "@keystonehq/metamask-airgapped-keyring": "npm:^0.13.1" "@lavamoat/allow-scripts": "npm:^3.0.4" @@ -26079,26 +26079,26 @@ __metadata: "@lgbot/madge": "npm:^6.2.0" "@lydell/node-pty": "npm:^1.0.1" "@material-ui/core": "npm:^4.11.0" - "@metamask-institutional/custody-controller": "npm:^0.2.31" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/extension": "npm:^0.3.27" - "@metamask-institutional/institutional-features": "npm:^1.3.5" + "@metamask-institutional/custody-controller": "npm:^0.3.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/extension": "npm:^0.3.28" + "@metamask-institutional/institutional-features": "npm:^1.3.6" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.1" "@metamask-institutional/rpc-allowlist": "npm:^1.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/transaction-update": "npm:^0.2.5" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/transaction-update": "npm:^0.2.6" + "@metamask-institutional/types": "npm:^1.2.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.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" - "@metamask/bitcoin-wallet-snap": "npm:^0.6.0" + "@metamask/bitcoin-wallet-snap": "npm:^0.8.1" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" @@ -26117,9 +26117,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.4.0" "@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" @@ -26127,12 +26127,13 @@ __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/json-rpc-engine": "npm:^10.0.0" + "@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" - "@metamask/message-signing-snap": "npm:^0.3.3" + "@metamask/message-signing-snap": "npm:^0.4.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/name-controller": "npm:^8.0.0" "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch" @@ -26146,27 +26147,28 @@ __metadata: "@metamask/phishing-warning": "npm:^4.0.0" "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.35.1" - "@metamask/preinstalled-example-snap": "npm:^0.1.0" + "@metamask/preferences-controller": "npm:^13.0.2" + "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/rpc-errors": "npm:^7.0.0" "@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/selected-network-controller": "npm:^18.0.2" + "@metamask/signature-controller": "npm:^20.0.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.7.0" - "@metamask/snaps-execution-environments": "npm:^6.7.2" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "@metamask/snaps-controllers": "npm:^9.11.1" + "@metamask/snaps-execution-environments": "npm:^6.9.1" + "@metamask/snaps-rpc-methods": "npm:^11.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/test-bundler": "npm:^1.0.0" - "@metamask/test-dapp": "npm:^8.4.0" - "@metamask/transaction-controller": "npm:^37.0.0" + "@metamask/test-dapp": "npm:8.7.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" @@ -26202,11 +26204,12 @@ __metadata: "@storybook/theming": "npm:^7.6.20" "@swc/core": "npm:1.4.11" "@swc/helpers": "npm:^0.5.7" + "@testing-library/dom": "npm:^7.31.2" "@testing-library/jest-dom": "npm:^5.11.10" "@testing-library/react": "npm:^10.4.8" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:^14.4.3" - "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch" + "@trezor/connect-web": "npm:^9.4.0" "@tsconfig/node20": "npm:^20.1.2" "@types/babelify": "npm:^7.3.7" "@types/browserify": "npm:^12.0.37" @@ -26258,6 +26261,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" @@ -26301,10 +26305,10 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.2.0" eslint-plugin-storybook: "npm:^0.6.15" eta: "npm:^3.2.0" + eth-chainlist: "npm:~0.0.498" eth-ens-namehash: "npm:^2.0.8" eth-lattice-keyring: "npm:^0.12.4" eth-method-registry: "npm:^4.0.0" - eth-rpc-errors: "npm:^4.0.2" ethereumjs-util: "npm:^7.0.10" ethers: "npm:5.7.0" extension-port-stream: "npm:^3.0.0" @@ -26329,7 +26333,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" @@ -26342,7 +26346,6 @@ __metadata: jest-environment-jsdom: "patch:jest-environment-jsdom@npm%3A29.7.0#~/.yarn/patches/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b.patch" jest-junit: "npm:^14.0.1" jsdom: "npm:^16.7.0" - json-rpc-engine: "npm:^6.1.0" json-rpc-middleware-stream: "npm:^5.0.1" json-schema-to-ts: "npm:^3.0.1" koa: "npm:^2.7.0" @@ -26356,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" @@ -26392,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" @@ -26970,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" @@ -29695,15 +29686,15 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^25.5.0": - version: 25.5.0 - resolution: "pretty-format@npm:25.5.0" +"pretty-format@npm:^26.6.2": + version: 26.6.2 + resolution: "pretty-format@npm:26.6.2" dependencies: - "@jest/types": "npm:^25.5.0" + "@jest/types": "npm:^26.6.2" ansi-regex: "npm:^5.0.0" ansi-styles: "npm:^4.0.0" - react-is: "npm:^16.12.0" - checksum: 10/da9e79b2b98e48cabdb0d5b090993a5677969565be898c06ffe38ec792bf1f0c0fcf5f752552eb039b03e7cad2203347208a9b0b132e4a401e6eac655d061b31 + react-is: "npm:^17.0.1" + checksum: 10/94a4c661bf77ed7c448d064c5af35796acbd972a33cff8a38030547ac396087bcd47f2f6e530824486cf4c8e9d9342cc8dd55fd068f135b19325b51e0cd06f87 languageName: node linkType: hard @@ -30509,14 +30500,14 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.0": +"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10/5aa564a1cde7d391ac980bedee21202fc90bdea3b399952117f54fb71a932af1e5902020144fb354b4690b2414a0c7aafe798eb617b76a3d441d956db7726fdf languageName: node linkType: hard -"react-is@npm:^17.0.0, react-is@npm:^17.0.2": +"react-is@npm:^17.0.0, react-is@npm:^17.0.1, react-is@npm:^17.0.2": version: 17.0.2 resolution: "react-is@npm:17.0.2" checksum: 10/73b36281e58eeb27c9cc6031301b6ae19ecdc9f18ae2d518bdb39b0ac564e65c5779405d623f1df9abf378a13858b79442480244bd579968afc1faf9a2ce5e05 @@ -30692,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" @@ -30725,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 @@ -32880,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" @@ -35449,6 +35472,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "undici@npm:5.28.4": version: 5.28.4 resolution: "undici@npm:5.28.4" @@ -37129,7 +37159,22 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:8.17.1, ws@npm:>=8.14.2, ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.2.3, ws@npm:^8.5.0, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:>=8.14.2, ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.2.3, ws@npm:^8.5.0, ws@npm:^8.8.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + languageName: node + linkType: hard + +"ws@npm:8.17.1": version: 8.17.1 resolution: "ws@npm:8.17.1" peerDependencies: @@ -37445,10 +37490,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4": - version: 3.22.4 - resolution: "zod@npm:3.22.4" - checksum: 10/73622ca36a916f785cf528fe612a884b3e0f183dbe6b33365a7d0fc92abdbedf7804c5e2bd8df0a278e1472106d46674281397a3dd800fa9031dc3429758c6ac +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 languageName: node linkType: hard