diff --git a/.circleci/config.yml b/.circleci/config.yml index 74815818582f..aa13dea93b75 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,15 +87,15 @@ aliases: cat ${HOME}/project/.circleci/scripts/enable-vnc.sh >> ~/.bashrc fi - # Check if MMI Optional tests should run - - &check-mmi-optional - name: Check if MMI Optional tests should run + # Check if MMI tests should run + - &check-mmi-trigger + name: Check if MMI tests should run command: | - RUN_MMI_OPTIONAL=$(cat ./RUN_MMI_OPTIONAL) - if [[ "${RUN_MMI_OPTIONAL}" == "true" ]]; then - echo "Running MMI Optional tests" + source mmi_trigger.env + if [ "${run_mmi_tests}" == "true" ]; then + echo "Running MMI tests" else - echo "Skipping MMI Optional tests" + echo "Skipping MMI tests" circleci step halt fi @@ -114,7 +114,7 @@ workflows: - trigger-beta-build: requires: - prep-deps - - check-pr-tag + - check-mmi-trigger - prep-deps - get-changed-files-with-git-diff: filters: @@ -179,7 +179,7 @@ workflows: - prep-build-test-mmi-playwright: requires: - prep-deps - - check-pr-tag + - check-mmi-trigger - prep-build-storybook: requires: - prep-deps @@ -231,7 +231,7 @@ workflows: requires: - prep-build-test-mmi - get-changed-files-with-git-diff - - test-e2e-mmi-playwright - OPTIONAL: + - test-e2e-mmi-playwright: requires: - prep-build-test-mmi-playwright - test-e2e-chrome-rpc-mmi: @@ -421,39 +421,6 @@ jobs: name: Create GitHub Pull Request for version command: .circleci/scripts/release-create-release-pr.sh - check-pr-tag: - docker: - - image: cimg/base:stable - steps: - - run: - name: Check for MMI Team Tag - command: | - #!/bin/bash - - GH_LABEL=team-mmi - if [ -z "$CIRCLE_PULL_REQUESTS" ]; then - echo "Skipping tag check; this is not a PR." - echo "false" > ./RUN_MMI_OPTIONAL - exit 0 - fi - - echo $CIRCLE_PULL_REQUESTS | sed 's/,/\n/g' - - # See if any associated PRs have matching label - HAS_MATCHING_PR=$(echo $CIRCLE_PULL_REQUESTS \ - | sed -e 's#,#\n#g' -e 's#/github.com/#/api.github.com/repos/#g' -e 's#/pull/#/pulls/#g' \ - | xargs -n1 curl -s \ - | jq -s "map((.labels|map(select(.name==\"${GH_LABEL}\"))))|flatten|length > 0") - - echo "${GH_LABEL} tag presence: ${HAS_MATCHING_PR}" - - # assign the RUN_MMI_OPTIONAL variable - echo "${HAS_MATCHING_PR}" > ./RUN_MMI_OPTIONAL - - persist_to_workspace: - root: . - paths: - - RUN_MMI_OPTIONAL - prep-deps: executor: node-browsers-medium steps: @@ -477,7 +444,7 @@ jobs: - gh/install - run: name: Install dependencies - command: .circleci/scripts/install-dependencies.sh + command: yarn --immutable - save_cache: key: dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }} paths: @@ -839,7 +806,7 @@ jobs: - run: corepack enable - attach_workspace: at: . - - run: *check-mmi-optional + - run: *check-mmi-trigger - run: name: Build MMI extension for Playwright e2e command: | @@ -854,7 +821,6 @@ jobs: - persist_to_workspace: root: . paths: - - RUN_MMI_OPTIONAL - dist-test-mmi-playwright - builds-test-mmi-playwright - store_artifacts: @@ -1306,7 +1272,7 @@ jobs: - store_test_results: path: test/test-results/e2e - test-e2e-mmi-playwright - OPTIONAL: + test-e2e-mmi-playwright: executor: playwright parallelism: 2 steps: @@ -1314,7 +1280,7 @@ jobs: - run: corepack enable - attach_workspace: at: . - - run: *check-mmi-optional + - run: *check-mmi-trigger - run: name: Move test build to dist command: mv ./dist-test-mmi-playwright ./dist @@ -1743,3 +1709,18 @@ jobs: - run: name: All Tests Passed command: echo 'whew - everything passed!' + + check-mmi-trigger: + executor: node-browsers-medium + steps: + - checkout + - run: + name: Check for MMI Team Label or Reviewer + command: ./.circleci/scripts/check_mmi_trigger.sh + - store_artifacts: + path: mmi_trigger.env + destination: mmi_trigger.env + - persist_to_workspace: + root: . + paths: + - mmi_trigger.env diff --git a/.circleci/scripts/check_mmi_trigger.sh b/.circleci/scripts/check_mmi_trigger.sh new file mode 100755 index 000000000000..2de2f69044d4 --- /dev/null +++ b/.circleci/scripts/check_mmi_trigger.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -eo pipefail + +# Ensure required environment variables are set +if [ -z "$CIRCLE_PULL_REQUEST" ] || [ -z "$GITHUB_TOKEN" ]; then + echo "This appears to be a fork or required environment variables are not set." + echo "Skipping MMI tests." + echo "run_mmi_tests=false" > mmi_trigger.env + exit 0 +fi + +# Extract PR number from the pull request URL +PR_NUMBER=$(echo "$CIRCLE_PULL_REQUEST" | awk -F'/' '{print $NF}') + +# Define repository details +REPO_OWNER="$CIRCLE_PROJECT_USERNAME" +REPO_NAME=$(basename "$CIRCLE_REPOSITORY_URL" .git) + +# Fetch PR details using GitHub API +PR_DETAILS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_NUMBER") + +# Fetch submitted reviews +SUBMITTED_REVIEWS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_NUMBER/reviews") + +# Check for label 'team-mmi' +LABEL_EXISTS=$(jq -r '.labels[]? | select(.name == "team-mmi") | length > 0' <<< "$PR_DETAILS") + +# Check for individual reviewer 'mmi' +REVIEWER_REQUESTED=$(jq -r '.requested_reviewers[]? | select(.login == "mmi") | length > 0' <<< "$PR_DETAILS") + +# Check for team reviewer 'mmi' +TEAM_REQUESTED=$(jq -r '.requested_teams[]? | select(.slug == "mmi") | length > 0' <<< "$PR_DETAILS") + +# Check if 'mmi' submitted a review +REVIEWER_SUBMITTED=$(jq -r '.[]? | select(.user.login == "mmi") | length > 0' <<< "$SUBMITTED_REVIEWS") + +# Determine which condition was met and trigger tests if needed +if [[ "$LABEL_EXISTS" == "true" || "$REVIEWER_REQUESTED" == "true" || "$TEAM_REQUESTED" == "true" || "$REVIEWER_SUBMITTED" == "true" ]]; then + echo "run_mmi_tests=true" > mmi_trigger.env + + # Log exactly which condition was met + echo "Conditions met:" + if [[ "$LABEL_EXISTS" == "true" ]]; then + echo "- Label 'team-mmi' found." + fi + if [[ "$REVIEWER_REQUESTED" == "true" ]]; then + echo "- Reviewer 'mmi' requested." + fi + if [[ "$TEAM_REQUESTED" == "true" ]]; then + echo "- Team 'mmi' requested." + fi + if [[ "$REVIEWER_SUBMITTED" == "true" ]]; then + echo "- Reviewer 'mmi' submitted a review." + fi +else + echo "run_mmi_tests=false" > mmi_trigger.env + echo "Skipping MMI tests: Neither the 'team-mmi' label was found nor a reviewer from the 'MetaMask/mmi' team was assigned." +fi diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts index 43435db17418..f4437d6154db 100644 --- a/.circleci/scripts/git-diff-develop.ts +++ b/.circleci/scripts/git-diff-develop.ts @@ -20,6 +20,7 @@ type PRInfo = { ref: string; }; body: string; + labels: { name: string }[]; }; /** @@ -123,7 +124,7 @@ async function storeGitDiffOutputAndPrBody() { fs.mkdirSync(CHANGED_FILES_DIR, { recursive: true }); console.log( - `Determining whether this run is for a PR targeting ${MAIN_BRANCH}`, + `Determining whether to run git diff...`, ); if (!PR_NUMBER) { console.log('Not a PR, skipping git diff'); @@ -140,6 +141,9 @@ async function storeGitDiffOutputAndPrBody() { console.log(`This is for a PR targeting '${baseRef}', skipping git diff`); writePrBodyToFile(prInfo.body); return; + } else if (prInfo.labels.some(label => label.name === 'skip-e2e-quality-gate')) { + console.log('PR has the skip-e2e-quality-gate label, skipping git diff'); + return; } console.log('Attempting to get git diff...'); diff --git a/.circleci/scripts/install-dependencies.sh b/.circleci/scripts/install-dependencies.sh deleted file mode 100755 index 35b7a690fa0c..000000000000 --- a/.circleci/scripts/install-dependencies.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -IS_NON_FORK_DRAFT='false' - -if [[ -n $CIRCLE_PULL_REQUEST ]] && gh auth status -then - PR_NUMBER="${CIRCLE_PULL_REQUEST##*/}" - if [ -n "$PR_NUMBER" ] - then - IS_NON_FORK_DRAFT="$(gh pr view --json isDraft --jq '.isDraft' "$PR_NUMBER")" - fi -fi - -# Build query to see whether there are any "preview-like" packages in the manifest -# A "preview-like" package is a `@metamask`-scoped package with a prerelease version that has no period. -QUERY='.dependencies + .devDependencies' # Get list of all dependencies -QUERY+=' | with_entries( select(.key | startswith("@metamask") ) )' # filter to @metamask-scoped packages -QUERY+=' | to_entries[].value' # Get version ranges -QUERY+=' | select(test("^\\d+\\.\\d+\\.\\d+-[^.]+$"))' # Get pinned versions where the prerelease part has no "." - -# Use `-e` flag so that exit code indicates whether any matches were found -if jq -e "${QUERY}" < ./package.json -then - echo "Preview builds detected" - HAS_PREVIEW_BUILDS='true' -else - echo "No preview builds detected" - HAS_PREVIEW_BUILDS='false' -fi - -if [[ $IS_NON_FORK_DRAFT == 'true' && $HAS_PREVIEW_BUILDS == 'true' ]] -then - # Use GitHub registry on draft PRs, allowing the use of preview builds - echo "Installing with preview builds" - METAMASK_NPM_REGISTRY=https://npm.pkg.github.com yarn --immutable -else - echo "Installing without preview builds" - yarn --immutable -fi diff --git a/.depcheckrc.yml b/.depcheckrc.yml index d0d6eac5b5bc..50b79a78ec30 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -81,6 +81,8 @@ ignores: # trezor - 'ts-mixer' - '@testing-library/dom' + - 'mini-css-extract-plugin' + - 'webpack-cli' # files depcheck should not parse ignorePatterns: diff --git a/.eslintrc.js b/.eslintrc.js index 846158a741ef..4aa9ef688592 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -472,5 +472,30 @@ module.exports = { '@metamask/design-tokens/color-no-hex': 'off', }, }, + { + files: ['ui/pages/confirmations/**/*.{js,ts,tsx}'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: `ImportSpecifier[imported.name=/${[ + 'getConversionRate', + 'getCurrentChainId', + 'getNativeCurrency', + 'getNetworkIdentifier', + 'getNftContracts', + 'getNfts', + 'getProviderConfig', + 'getRpcPrefsForCurrentProvider', + 'getUSDConversionRate', + 'isCurrentProviderCustom', + ] + .map((method) => `(${method})`) + .join('|')}/]`, + message: 'Avoid using global network selectors in confirmations', + }, + ], + }, + }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e14d27619a07..f37a101e6cb2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -52,7 +52,12 @@ privacy-snapshot.json @MetaMask/extension-privacy-reviewers .devcontainer/ @MetaMask/library-admins @HowardBraham @plasmacorral # Confirmations team to own code for confirmations on UI. -ui/pages/confirmations @MetaMask/confirmations +app/scripts/lib/ppom @MetaMask/confirmations +app/scripts/lib/signature @MetaMask/confirmations +app/scripts/lib/transaction/decode @MetaMask/confirmations +app/scripts/lib/transaction/metrics.* @MetaMask/confirmations +app/scripts/lib/transaction/util.* @MetaMask/confirmations +ui/pages/confirmations @MetaMask/confirmations # MMI team is responsible for code related with Institutioanl version of MetaMask ui/pages/institutional @MetaMask/mmi diff --git a/.github/workflows/add-team-label.yml b/.github/workflows/add-team-label.yml index 2046456ef426..b5d9eaa18603 100644 --- a/.github/workflows/add-team-label.yml +++ b/.github/workflows/add-team-label.yml @@ -7,8 +7,6 @@ on: jobs: add-team-label: - uses: metamask/github-tools/.github/workflows/add-team-label.yml@058012b49ff2fbd9649c566ba43b29497f93b21d - permissions: - pull-requests: write + uses: metamask/github-tools/.github/workflows/add-team-label.yml@18af6e4b56a18230d1792480e249ebc50b324927 secrets: - PERSONAL_ACCESS_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }} + TEAM_LABEL_TOKEN: ${{ secrets.TEAM_LABEL_TOKEN }} diff --git a/.gitignore b/.gitignore index 1671e69527e0..074f4076a7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ html-report/ /app/images/branding /changed-files + +# UI Integration tests +test/integration/config/assets diff --git a/.metamaskrc.dist b/.metamaskrc.dist index fc2a5a831a4b..cbb55baedc7b 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -4,6 +4,10 @@ ; This variable is required INFURA_PROJECT_ID=00000000000 +; This variable is not required but it's necessary for the storybook +; to render stories that use onchain data. +INFURA_STORYBOOK_PROJECT_ID= + ;PASSWORD=METAMASK PASSWORD ;SEGMENT_WRITE_KEY= ;BRIDGE_USE_DEV_APIS= diff --git a/.storybook/main.js b/.storybook/main.js index d63d924aa2e2..2b4384250c9c 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,6 +1,9 @@ const path = require('path'); const { ProvidePlugin } = require('webpack'); const CopyWebpackPlugin = require('copy-webpack-plugin'); +const dotenv = require('dotenv'); +dotenv.config({ path: path.resolve(__dirname, '../.metamaskrc') }); + module.exports = { core: { disableTelemetry: true, @@ -29,6 +32,7 @@ module.exports = { env: (config) => ({ ...config, ENABLE_CONFIRMATION_REDESIGN: true, + INFURA_PROJECT_ID: process.env.INFURA_STORYBOOK_PROJECT_ID || '', }), // Uses babel.config.js settings and prevents "Missing class properties transform" error babel: async (options) => ({ @@ -48,10 +52,16 @@ module.exports = { config.resolve.alias['../../../../../../store/actions'] = require.resolve( '../ui/__mocks__/actions.js', ); + config.resolve.alias['../../../store/actions'] = require.resolve( + '../ui/__mocks__/actions.js', + ); // Import within controller-utils crashes storybook. config.resolve.alias['@ethereumjs/util'] = require.resolve( '../ui/__mocks__/ethereumjs-util.js', ); + config.resolve.alias['./useNftCollectionsMetadata'] = require.resolve( + '../ui/__mocks__/useNftCollectionsMetadata.js', + ); config.resolve.fallback = { child_process: false, constants: false, @@ -86,7 +96,7 @@ module.exports = { sourceMap: true, implementation: require('sass-embedded'), sassOptions: { - includePaths: ['ui/css/', 'node_modules/',], + includePaths: ['ui/css/', 'node_modules/'], }, }, }, @@ -96,12 +106,7 @@ module.exports = { new CopyWebpackPlugin({ patterns: [ { - from: path.join( - 'ui', - 'css', - 'utilities', - 'fonts/', - ), + from: path.join('ui', 'css', 'utilities', 'fonts/'), to: 'fonts', }, { diff --git a/.storybook/preview.js b/.storybook/preview.js index b46c91339273..525c364f2072 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -15,6 +15,7 @@ import MetaMetricsProviderStorybook from './metametrics'; import testData from './test-data.js'; import { Router } from 'react-router-dom'; import { createBrowserHistory } from 'history'; +import { MemoryRouter } from 'react-router-dom'; import { setBackgroundConnection } from '../ui/store/background-connection'; import { metamaskStorybookTheme } from './metamask-storybook-theme'; import { DocsContainer } from '@storybook/addon-docs'; @@ -147,7 +148,7 @@ const metamaskDecorator = (story, context) => { return ( - + { - + ); }; diff --git a/.storybook/test-data.js b/.storybook/test-data.js index cbcebb6347ed..13006e5d1ff7 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1596,6 +1596,7 @@ const state = { }, }, }, + openSeaEnabled: true, }, appState: { shouldClose: false, diff --git a/.yarn/patches/@babel-core-npm-7.23.2-b93f586907.patch b/.yarn/patches/@babel-core-npm-7.23.2-b93f586907.patch deleted file mode 100644 index fdae8d6b2b4e..000000000000 --- a/.yarn/patches/@babel-core-npm-7.23.2-b93f586907.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/lib/index.js b/lib/index.js -index 64ff8344f6280d20988f8c3c81e1f248a1869e53..6739e7bd2271be6b861479ec384bbd007bdb5df8 100644 ---- a/lib/index.js -+++ b/lib/index.js -@@ -222,7 +222,6 @@ var _transform = require("./transform.js"); - var _transformFile = require("./transform-file.js"); - var _transformAst = require("./transform-ast.js"); - var _parse = require("./parse.js"); --var thisFile = require("./index.js"); - ; - const version = "7.23.2"; - exports.version = version; diff --git a/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch b/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch new file mode 100644 index 000000000000..5010df3a0e88 --- /dev/null +++ b/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch @@ -0,0 +1,12 @@ +diff --git a/lib/index.js b/lib/index.js +index 55b58e10eef589ff80ae33ebd1f1efe488b01153..e919c190d33ab9563f1364667fb4f5894bb6435d 100644 +--- a/lib/index.js ++++ b/lib/index.js +@@ -211,7 +211,6 @@ var _transform = require("./transform.js"); + var _transformFile = require("./transform-file.js"); + var _transformAst = require("./transform-ast.js"); + var _parse = require("./parse.js"); +-var thisFile = require("./index.js"); + ; + const version = exports.version = "7.25.9"; + const resolvePlugin = (name, dirname) => resolvers.resolvePlugin(name, dirname, false).filepath; diff --git a/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch b/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch similarity index 87% rename from .yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch rename to .yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch index 0fb2e4f26622..4fec43fdb0c3 100644 --- a/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch +++ b/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch @@ -1,10 +1,10 @@ diff --git a/helpers/construct.js b/helpers/construct.js -index 771e1d7952e80f11619424fbabb3744b959ffa49..5fe152bc1129bd8c8b7bb217ca1972ac4e089051 100644 +index aee8e70448824f509d6605e2dfa4455167442f21..00a69eba8d4c15a1f9aa318a50abb96c2ec447d9 100644 --- a/helpers/construct.js +++ b/helpers/construct.js -@@ -1,10 +1,21 @@ +@@ -1,10 +1,22 @@ + var isNativeReflectConstruct = require("./isNativeReflectConstruct.js"); -var setPrototypeOf = require("./setPrototypeOf.js"); --var isNativeReflectConstruct = require("./isNativeReflectConstruct.js"); -function _construct(t, e, r) { - if (isNativeReflectConstruct()) return Reflect.construct.apply(null, arguments); - var o = [null]; diff --git a/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch b/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch similarity index 100% rename from .yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch rename to .yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch diff --git a/.yarnrc.yml b/.yarnrc.yml index cc0c959e2722..8e12d8037c6a 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -119,14 +119,6 @@ npmAuditIgnoreAdvisories: - 'react-beautiful-dnd (deprecation)' # New package name format for new versions: @ethereumjs/wallet. - 'ethereumjs-wallet (deprecation)' -npmRegistries: - 'https://npm.pkg.github.com': - npmAlwaysAuth: true - npmAuthToken: '${GITHUB_PACKAGE_READ_TOKEN-}' - -npmScopes: - metamask: - npmRegistryServer: '${METAMASK_NPM_REGISTRY:-https://registry.yarnpkg.com}' plugins: - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index af63a4ed61d3..287e9cd9c8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,172 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.6.1] +### Uncategorized +- fix: Return to send page with different asset types ([#28382](https://github.com/MetaMask/metamask-extension/pull/28382)) +- test: [POM] Refactor import account e2e tests to use Page Object Model ([#28325](https://github.com/MetaMask/metamask-extension/pull/28325)) +- feat(1852): Implement sentry user report on error screen ([#27857](https://github.com/MetaMask/metamask-extension/pull/27857)) +- fix: disable buy for btc testnet accounts ([#28341](https://github.com/MetaMask/metamask-extension/pull/28341)) +- fix: Address design review for ERC20 token send ([#28212](https://github.com/MetaMask/metamask-extension/pull/28212)) +- refactor: remove global network usage from transaction confirmations ([#28236](https://github.com/MetaMask/metamask-extension/pull/28236)) +- fix: gas limit estimation ([#28327](https://github.com/MetaMask/metamask-extension/pull/28327)) +- build: update yarn to v4.5.1 ([#28365](https://github.com/MetaMask/metamask-extension/pull/28365)) +- fix: Bug 28347 - Privacy mode tweaks ([#28367](https://github.com/MetaMask/metamask-extension/pull/28367)) +- fix: mv2 firefox csp header ([#27770](https://github.com/MetaMask/metamask-extension/pull/27770)) +- perf: ensure `setupLocale` doesn't fetch `_locales/en/messages.json` twice ([#26553](https://github.com/MetaMask/metamask-extension/pull/26553)) +- fix: bump `@metamask/queued-request-controller` with patch fix ([#28355](https://github.com/MetaMask/metamask-extension/pull/28355)) +- fix: Revert "fix: Negate privacy mode in Send screen" ([#28360](https://github.com/MetaMask/metamask-extension/pull/28360)) +- fix: disable account syncing ([#28359](https://github.com/MetaMask/metamask-extension/pull/28359)) +- feat: Convert mmi controller to a non-controller ([#27983](https://github.com/MetaMask/metamask-extension/pull/27983)) +- fix: Updates to the simulations component ([#28107](https://github.com/MetaMask/metamask-extension/pull/28107)) +- refactor: rename SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST ([#28337](https://github.com/MetaMask/metamask-extension/pull/28337)) +- chore: adds Solana snap to preinstall list ([#28141](https://github.com/MetaMask/metamask-extension/pull/28141)) +- feat: Show network badge in detected tokens modal ([#28231](https://github.com/MetaMask/metamask-extension/pull/28231)) +- fix: Negate privacy mode in Send screen ([#28248](https://github.com/MetaMask/metamask-extension/pull/28248)) +- feat: adds solana feature, code fence ([#28320](https://github.com/MetaMask/metamask-extension/pull/28320)) +- build(webpack): fix cache issues in webpack build by updating `html-bundler-webpack-plugin` to v4.4.1 ([#28225](https://github.com/MetaMask/metamask-extension/pull/28225)) +- feat: team-label-token ([#28134](https://github.com/MetaMask/metamask-extension/pull/28134)) +- chore: add Solana shared utilities and constants ([#28269](https://github.com/MetaMask/metamask-extension/pull/28269)) +- chore: Remove STX opt in modal ([#28291](https://github.com/MetaMask/metamask-extension/pull/28291)) +- chore: revert commit `3da34f4` (feat: btc e2e tests (#27986)) ([#27986](https://github.com/MetaMask/metamask-extension/pull/27986)) +- chore: e2e quality gate enhancement ([#28206](https://github.com/MetaMask/metamask-extension/pull/28206)) +- chore: adding e2e tests for NFT permit ([#28004](https://github.com/MetaMask/metamask-extension/pull/28004)) +- feat: Enable simulation metrics for redesign transactions ([#28280](https://github.com/MetaMask/metamask-extension/pull/28280)) +- fix: GasDetailItem invalid paddingStart prop ([#28281](https://github.com/MetaMask/metamask-extension/pull/28281)) +- fix: use transaction address to get lock for custom nonce ([#28272](https://github.com/MetaMask/metamask-extension/pull/28272)) +- fix: flaky test `Phishing Detection Via Iframe should redirect users to the the MetaMask Phishing Detection page when an iframe domain is on the phishing blocklist` ([#28293](https://github.com/MetaMask/metamask-extension/pull/28293)) +- chore: add the gas_included prop into Quotes Requested event ([#28295](https://github.com/MetaMask/metamask-extension/pull/28295)) +- test: [POM] Refactor e2e tests to use onboarding flows defined in Page Object Models ([#28202](https://github.com/MetaMask/metamask-extension/pull/28202)) +- feat: btc e2e tests ([#27986](https://github.com/MetaMask/metamask-extension/pull/27986)) +- fix: remove scroll-to-bottom requirement in redesigned transaction confirmations ([#27910](https://github.com/MetaMask/metamask-extension/pull/27910)) +- chore: Add gravity logo and image mappings ([#28306](https://github.com/MetaMask/metamask-extension/pull/28306)) +- chore: Bump Snaps packages ([#28215](https://github.com/MetaMask/metamask-extension/pull/28215)) +- feat: Add simulation metrics to "Transaction Submitted" and "Transaction Finalized" events ([#28240](https://github.com/MetaMask/metamask-extension/pull/28240)) +- fix: smart transactions in redesigned confirmations ([#28273](https://github.com/MetaMask/metamask-extension/pull/28273)) +- fix: unit flaky test `AddContact component › should disable submit button when input is not a valid address` ([#27941](https://github.com/MetaMask/metamask-extension/pull/27941)) +- fix: Hide fiat values on test networks ([#28219](https://github.com/MetaMask/metamask-extension/pull/28219)) +- chore: display bridge quotes ([#28031](https://github.com/MetaMask/metamask-extension/pull/28031)) +- fix: Permit message, dataTree value incorrectly using default ERC20 decimals for non-ERC20 token values ([#28142](https://github.com/MetaMask/metamask-extension/pull/28142)) +- fix: ignore error when getTokenStandardAndDetails fails ([#28030](https://github.com/MetaMask/metamask-extension/pull/28030)) +- fix: notification settings type ([#28271](https://github.com/MetaMask/metamask-extension/pull/28271)) +- chore: use accounts api for token detection ([#28254](https://github.com/MetaMask/metamask-extension/pull/28254)) +- fix: Fix alignment of long RPC labels in Networks menu ([#28244](https://github.com/MetaMask/metamask-extension/pull/28244)) +- feat: adds the experimental toggle for Solana ([#28190](https://github.com/MetaMask/metamask-extension/pull/28190)) +- feat: multi chain polling for token prices ([#28158](https://github.com/MetaMask/metamask-extension/pull/28158)) +- refactor: move `getInternalAccounts` from `selectors.js` to `accounts.ts` ([#27645](https://github.com/MetaMask/metamask-extension/pull/27645)) +- fix: Add different copy for tooltip when a snap is requesting a signature ([#27492](https://github.com/MetaMask/metamask-extension/pull/27492)) +- fix: Prevent coercing symbols to zero in the edit spending cap modal ([#28192](https://github.com/MetaMask/metamask-extension/pull/28192)) +- test: [POM] Migrate edit network rpc e2e tests and create related page class functions ([#28161](https://github.com/MetaMask/metamask-extension/pull/28161)) +- refactor: remove global network usage from signatures ([#28167](https://github.com/MetaMask/metamask-extension/pull/28167)) +- fix: margin on asset chart min/max indicators ([#27916](https://github.com/MetaMask/metamask-extension/pull/27916)) +- feat: add token verification source count and link to block explorer ([#27759](https://github.com/MetaMask/metamask-extension/pull/27759)) +- chore: Remove obsolete preview build support ([#27968](https://github.com/MetaMask/metamask-extension/pull/27968)) +- fix: Removing `warning` prop from settings ([#27990](https://github.com/MetaMask/metamask-extension/pull/27990)) +- chore: Adding installType to Sentry Tags for easy filtering ([#28084](https://github.com/MetaMask/metamask-extension/pull/28084)) +- chore: remove broken link in docs ([#28232](https://github.com/MetaMask/metamask-extension/pull/28232)) +- fix: Error handling for the state log download failure ([#26999](https://github.com/MetaMask/metamask-extension/pull/26999)) +- feat: Upgrade alert controller to base controller v2 ([#28054](https://github.com/MetaMask/metamask-extension/pull/28054)) +- chore: improve token lookup performance in `useAccountTotalFiatBalance` ([#28233](https://github.com/MetaMask/metamask-extension/pull/28233)) +- chore: Master sync ([#28222](https://github.com/MetaMask/metamask-extension/pull/28222)) +- Merge origin/develop into master-sync +- refactor: move `getSelectedInternalAccount` from `selectors.js` to `accounts.ts` ([#27644](https://github.com/MetaMask/metamask-extension/pull/27644)) +- feat: Improve provider method metrics for add/switch chain ([#28214](https://github.com/MetaMask/metamask-extension/pull/28214)) +- fix: Fix left-aligned fullscreen UI ([#28218](https://github.com/MetaMask/metamask-extension/pull/28218)) +- test: add ui render for debug ui integration tests ([#27621](https://github.com/MetaMask/metamask-extension/pull/27621)) +- feat: poll native currency prices across chains ([#28196](https://github.com/MetaMask/metamask-extension/pull/28196)) +- test: Fix data deletion e2e tests ([#28221](https://github.com/MetaMask/metamask-extension/pull/28221)) +- feat: Bump `QueuedRequestController` from `^2.0.0` to `^7.0.0`  ([#28090](https://github.com/MetaMask/metamask-extension/pull/28090)) +- chore: Add a new transaction event prop ([#28153](https://github.com/MetaMask/metamask-extension/pull/28153)) +- feat: Copy updates to satisfy UK regulation requirements ([#28157](https://github.com/MetaMask/metamask-extension/pull/28157)) +- feat: Token Network Filter UI [Extension] ([#27884](https://github.com/MetaMask/metamask-extension/pull/27884)) +- fix: flaky test `BTC Account - Overview has balance` ([#28181](https://github.com/MetaMask/metamask-extension/pull/28181)) +- feat: Native asset send ([#27979](https://github.com/MetaMask/metamask-extension/pull/27979)) +- chore: poll for bridge quotes ([#28029](https://github.com/MetaMask/metamask-extension/pull/28029)) +- fix: Prevent coercing small spending caps to zero ([#28179](https://github.com/MetaMask/metamask-extension/pull/28179)) +- fix: Fix #28097 - Prevent redirect after adding network in Onboarding Settings ([#28165](https://github.com/MetaMask/metamask-extension/pull/28165)) +- feat: add privacy mode ([#28021](https://github.com/MetaMask/metamask-extension/pull/28021)) +- chore: update confirmations code ownership ([#27862](https://github.com/MetaMask/metamask-extension/pull/27862)) +- feat(snaps): Add `useDisplayName` hook ([#27868](https://github.com/MetaMask/metamask-extension/pull/27868)) +- chore: upgrade signature controller to remove global network ([#28063](https://github.com/MetaMask/metamask-extension/pull/28063)) +- feat: enable security alerts api ([#28040](https://github.com/MetaMask/metamask-extension/pull/28040)) +- feat: Add re-simulation logic ([#28104](https://github.com/MetaMask/metamask-extension/pull/28104)) +- chore: update bridge quote request on input change ([#28028](https://github.com/MetaMask/metamask-extension/pull/28028)) +- fix: Updated network message on Review Permission and Connections page ([#28126](https://github.com/MetaMask/metamask-extension/pull/28126)) +- chore: bump asset controllers to 39 + polling API ([#28025](https://github.com/MetaMask/metamask-extension/pull/28025)) +- fix: incorrect standard swap gas fee estimation ([#28127](https://github.com/MetaMask/metamask-extension/pull/28127)) +- feat: Capture 3 existing properties within non-anonymous transaction … ([#28144](https://github.com/MetaMask/metamask-extension/pull/28144)) +- refactor: remove global network usage from transaction simulation ([#27895](https://github.com/MetaMask/metamask-extension/pull/27895)) +- test(ramps): fixes btc native token test ([#27601](https://github.com/MetaMask/metamask-extension/pull/27601)) +- fix: Reduce gas limit fallback from 95% to 35% of the block gas limit on failed gas limit estimations ([#27954](https://github.com/MetaMask/metamask-extension/pull/27954)) +- refactor: clean up profile sync hooks ([#28132](https://github.com/MetaMask/metamask-extension/pull/28132)) +- chore: ignore warning for ethereumjs-wallet ([#28145](https://github.com/MetaMask/metamask-extension/pull/28145)) +- test: [Snaps E2E] Unified methods and clean up snaps e2e tests ([#27684](https://github.com/MetaMask/metamask-extension/pull/27684)) +- feat: added test network as selected network if globally selected for connection Request ([#27980](https://github.com/MetaMask/metamask-extension/pull/27980)) +- chore: update @metamask/bitcoin-wallet-snap to 0.8.2 ([#28135](https://github.com/MetaMask/metamask-extension/pull/28135)) +- chore: small storybook and docs updates to SensitiveText component ([#28089](https://github.com/MetaMask/metamask-extension/pull/28089)) +- feat(NOTIFY-1260): enable account syncing ([#28120](https://github.com/MetaMask/metamask-extension/pull/28120)) +- chore: bridge-api fetchBridgeQuotes util ([#28027](https://github.com/MetaMask/metamask-extension/pull/28027)) +- feat: update phishing controller version ([#28131](https://github.com/MetaMask/metamask-extension/pull/28131)) +- fix: broken not existing type file import ([#28055](https://github.com/MetaMask/metamask-extension/pull/28055)) +- test: blockaid update version and reenable specs ([#28121](https://github.com/MetaMask/metamask-extension/pull/28121)) +- fix: Reduce usage of scientific notation ([#27992](https://github.com/MetaMask/metamask-extension/pull/27992)) +- test: [POM] Migrate onboarding infura call privacy e2e tests ([#28079](https://github.com/MetaMask/metamask-extension/pull/28079)) +- feat: share the same user storage mock instance in tests ([#28119](https://github.com/MetaMask/metamask-extension/pull/28119)) +- chore: Using button icon component for clikable icons ([#28082](https://github.com/MetaMask/metamask-extension/pull/28082)) +- feat: convert MetaMetricsController to typescript ([#28072](https://github.com/MetaMask/metamask-extension/pull/28072)) +- feat: improved way to trigger mmi e2e tests ([#27932](https://github.com/MetaMask/metamask-extension/pull/27932)) +- test: allow more simple findElement by data-testid ([#28065](https://github.com/MetaMask/metamask-extension/pull/28065)) +- fix: json-rpc-middleware-stream@^5 -> @metamask/json-rpc-middleware-stream@^8 ([#28060](https://github.com/MetaMask/metamask-extension/pull/28060)) +- fix(devDeps): babel@7.23.2->7.25.9 ([#28068](https://github.com/MetaMask/metamask-extension/pull/28068)) +- feat: better storybook stories for the notification pages ([#27861](https://github.com/MetaMask/metamask-extension/pull/27861)) +- fix: update storybook to support NFT images ([#28105](https://github.com/MetaMask/metamask-extension/pull/28105)) +- fix: broken test `Vault Decryptor Page is able to decrypt the vault uploading the log file in the vault-decryptor webapp` ([#28098](https://github.com/MetaMask/metamask-extension/pull/28098)) +- test: Complete missing step for add a contact to the address book in existing E2E test ([#27959](https://github.com/MetaMask/metamask-extension/pull/27959)) +- feat(3419): sensitive text component ([#28056](https://github.com/MetaMask/metamask-extension/pull/28056)) +- test: Added e2e for switch network ([#27967](https://github.com/MetaMask/metamask-extension/pull/27967)) +- fix: c2 bypass ([#28057](https://github.com/MetaMask/metamask-extension/pull/28057)) +- feat: design changes in signature paged message section ([#28038](https://github.com/MetaMask/metamask-extension/pull/28038)) +- test: NOTIFY-1256 - Extending E2E tests for Account Sync ([#28067](https://github.com/MetaMask/metamask-extension/pull/28067)) +- feat: NFT token transfer ([#27955](https://github.com/MetaMask/metamask-extension/pull/27955)) +- test: notifications integration tests ([#28022](https://github.com/MetaMask/metamask-extension/pull/28022)) +- fix: disable notifications when basic functionality off ([#28045](https://github.com/MetaMask/metamask-extension/pull/28045)) +- chore: update stories for name component ([#28049](https://github.com/MetaMask/metamask-extension/pull/28049)) +- fix: flaky anti-pattern getText + assert 3 ([#28062](https://github.com/MetaMask/metamask-extension/pull/28062)) +- feat: add support for external links in feature announcements ([#26491](https://github.com/MetaMask/metamask-extension/pull/26491)) +- test: [POM] Create onboarding related page object modal base pages and migrate e2e tests ([#28036](https://github.com/MetaMask/metamask-extension/pull/28036)) +- docs: update debugging sentry step 3 ([#28034](https://github.com/MetaMask/metamask-extension/pull/28034)) +- fix: Support dynamic native token name on gas component ([#28048](https://github.com/MetaMask/metamask-extension/pull/28048)) +- fix: Fall back to token list for the token symbol ([#28003](https://github.com/MetaMask/metamask-extension/pull/28003)) +- fix: flaky anti-pattern getText + assert 2 ([#28043](https://github.com/MetaMask/metamask-extension/pull/28043)) +- feat: :sparkles: show a notification item in the settings page ([#26843](https://github.com/MetaMask/metamask-extension/pull/26843)) +- fix: Fix limited visibility of decrypt message ([#27622](https://github.com/MetaMask/metamask-extension/pull/27622)) +- fix(deps): @metamask/eth-json-rpc-filters@^8.0.0->^9.0.0 ([#27956](https://github.com/MetaMask/metamask-extension/pull/27956)) +- chore: Bump gridplus-sdk to 2.7.1 ([#28008](https://github.com/MetaMask/metamask-extension/pull/28008)) +- feat: new phishing warning UI with metrics ([#27942](https://github.com/MetaMask/metamask-extension/pull/27942)) +- fix(deps): @keystonehq/metamask-airgapped-keyring@^0.13.1->^0.14.1 ([#27952](https://github.com/MetaMask/metamask-extension/pull/27952)) +- fix: Gas changes for low Max base fee and Priority fee ([#28037](https://github.com/MetaMask/metamask-extension/pull/28037)) +- fix: adjust spacing of quote rate in swaps ([#28016](https://github.com/MetaMask/metamask-extension/pull/28016)) +- feat: enable preview token ([#27809](https://github.com/MetaMask/metamask-extension/pull/27809)) +- refactor: use `reselect`'s `createSelector` instead of going through `@redux/toolkit`, as the import names collide when trying to merge files. ([#27643](https://github.com/MetaMask/metamask-extension/pull/27643)) +- fix: storybook `getManifest` issue ([#28010](https://github.com/MetaMask/metamask-extension/pull/28010)) +- feat: bump @metamask/notification-services-controller from 0.7.0 to 0.11.0 ([#28017](https://github.com/MetaMask/metamask-extension/pull/28017)) +- refactor: remove global network usage from petnames ([#27946](https://github.com/MetaMask/metamask-extension/pull/27946)) +- chore: updated package ([#28002](https://github.com/MetaMask/metamask-extension/pull/28002)) +- feat(NOTIFY-1245): add account syncing E2E helpers & basic tests ([#28005](https://github.com/MetaMask/metamask-extension/pull/28005)) +- fix: Fix stream re-initialization ([#28024](https://github.com/MetaMask/metamask-extension/pull/28024)) +- refactor: routes.component.js and creation of ToastMaster ([#27735](https://github.com/MetaMask/metamask-extension/pull/27735)) +- fix: @metamask/eth-json-rpc-filters@^7.0.0->^8.0.0 ([#27917](https://github.com/MetaMask/metamask-extension/pull/27917)) +- refactor: remove relative imports to `selectors/index.js` from other selectors files ([#27642](https://github.com/MetaMask/metamask-extension/pull/27642)) +- refactor: remove circular dependency between `ui/ducks/custom-gas.js` and `ui/selectors/index.js` ([#27640](https://github.com/MetaMask/metamask-extension/pull/27640)) +- fix: Allow users to remove linea from networks list ([#27512](https://github.com/MetaMask/metamask-extension/pull/27512)) +- fix: prevent scrolling to account list item on send page ([#27934](https://github.com/MetaMask/metamask-extension/pull/27934)) +- feat: add ape token mainnet ([#27974](https://github.com/MetaMask/metamask-extension/pull/27974)) +- feat: removed feature flag for confirmations screen ([#27877](https://github.com/MetaMask/metamask-extension/pull/27877)) +- test: update notification date tests to be timezone agnostic ([#27925](https://github.com/MetaMask/metamask-extension/pull/27925)) +- fix: updated event name for site cell component ([#27981](https://github.com/MetaMask/metamask-extension/pull/27981)) +- fix(snaps): Adjust alignment of custom UI links ([#27957](https://github.com/MetaMask/metamask-extension/pull/27957)) +- fix(deps): gridplus-sdk@2.5.1->~2.6.0 ([#27973](https://github.com/MetaMask/metamask-extension/pull/27973)) + ## [12.6.0] ### Added - Added the APE network icon ([#27841](https://github.com/MetaMask/metamask-extension/pull/27841)) @@ -5303,7 +5469,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.6.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.6.1...HEAD +[12.6.1]: https://github.com/MetaMask/metamask-extension/compare/v12.6.0...v12.6.1 [12.6.0]: https://github.com/MetaMask/metamask-extension/compare/v12.5.1...v12.6.0 [12.5.1]: https://github.com/MetaMask/metamask-extension/compare/v12.5.0...v12.5.1 [12.5.0]: https://github.com/MetaMask/metamask-extension/compare/v12.4.2...v12.5.0 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index fe0c84afcfac..f62b15c08c93 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Benutzerdefiniertes Netzwerk hinzufügen" }, - "addEthereumChainConfirmationDescription": { - "message": "Dadurch kann dieses Netzwerk innerhalb MetaMask verwendet werden." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask überprüft keine benutzerdefinierten Netzwerke." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Erfahren Sie mehr über $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "Betrug und Sicherheitsrisiken im Netzwerk", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Dieser Seite das Hinzufügen eines Netzwerks erlauben?" - }, "addEthereumChainWarningModalHeader": { "message": "Fügen Sie diesen RPC-Anbieter nur hinzu, wenn Sie sich sicher sind, dass Sie ihm vertrauen können. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Jetzt kaufen" }, - "buyToken": { - "message": "$1 kaufen", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Beschleunigung der Gasgebühr bearbeiten" }, - "enable": { - "message": "Aktivieren" - }, "enableAutoDetect": { "message": " Automatische Erkennung aktivieren" }, @@ -1720,10 +1696,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Fehlerdetails", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Fehler beim Abrufen der Liste sicherer Ketten, bitte mit Vorsicht fortfahren." }, @@ -1735,14 +1707,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Versuchen Sie es erneut, indem Sie die Seite neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Versuchen Sie es erneut, indem das Popup schließen und neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask hat einen Fehler festgestellt.", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Funktionstyp" }, - "fundYourWallet": { - "message": "Versehen Sie Ihre Wallet mit Geldern" - }, - "fundYourWalletDescription": { - "message": "Legen Sie los, indem Sie Ihrer Wallet $1 hinzufügen.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gas" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Konto auf $1 ansehen" }, - "getStartedWithNFTs": { - "message": "Erhalten Sie $1 für den Kauf von NFTs", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Legen Sie mit NFTs los, indem Sie Ihrer Wallet $1 hinzufügen.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Zurück" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Anmelden mit" }, - "simulationDetailsFailed": { - "message": "Es ist ein Fehler beim Laden Ihrer Schätzung aufgetreten." - }, "simulationDetailsFiatNotAvailable": { "message": "Nicht verfügbar" }, "simulationDetailsIncomingHeading": { "message": "Sie erhalten" }, - "simulationDetailsNoBalanceChanges": { - "message": "Keine Änderungen für Ihre Wallet prognostiziert." - }, "simulationDetailsOutgoingHeading": { "message": "Sie senden" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Ressourcen" }, - "siweSignatureSimulationDetailInfo": { - "message": "Sie melden sich bei einer Website an und es sind keine Änderungen an Ihrem Konto vorgesehen." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Smart Transactions" }, - "smartTransactionsBenefit1": { - "message": "Erfolgsrate: 99,5 %" - }, - "smartTransactionsBenefit2": { - "message": "Spart Ihnen Geld" - }, - "smartTransactionsBenefit3": { - "message": "Updates in Echtzeit" - }, - "smartTransactionsDescription": { - "message": "Erzielen Sie mit Smart Transactions höhere Erfolgsraten, einen Frontrunning-Schutz und eine bessere Transparenz." - }, - "smartTransactionsDescription2": { - "message": "Nur auf Ethereum verfügbar. Sie können diese Funktion jederzeit in den Einstellungen aktivieren oder deaktivieren. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Verbesserter Transaktionsschutz" - }, "snapAccountCreated": { "message": "Konto erstellt" }, @@ -4858,9 +4779,6 @@ "message": "Kontaktieren Sie die Ersteller von $1 für weitere Unterstützung.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Einige Netzwerke können Sicherheits- und/oder Datenschutzrisiken bergen. Informieren Sie sich über die Risiken, bevor Sie ein Netzwerk hinzufügen und nutzen." - }, "somethingDoesntLookRight": { "message": "Scheint irgendetwas nicht in Ordnung zu sein? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Anteil" }, - "startYourJourney": { - "message": "Beginnen Sie Ihre Reise mit $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Legen Sie mit web3 los, indem Sie Ihrer Wallet $1 hinzufügen.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Fehler beim Abfragen der Statusprotokolle." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Aktivität anzeigen" }, - "viewAllDetails": { - "message": "Alle Details anzeigen" - }, "viewAllQuotes": { "message": "alle Angebote anzeigen" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 5c42a8d829b4..3bede7c85edb 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Προσθήκη προσαρμοσμένου δικτύου" }, - "addEthereumChainConfirmationDescription": { - "message": "Αυτό θα επιτρέψει σε αυτό το δίκτυο να χρησιμοποιηθεί στο MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "Το MetaMask δεν επαληθεύει τα προσαρμοσμένα δίκτυα." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Μάθετε για το $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "απάτες και κίνδυνοι ασφάλειας δικτύου", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Επιτρέπετε σε αυτήν την ιστοσελίδα να προσθέσει ένα δίκτυο;" - }, "addEthereumChainWarningModalHeader": { "message": "Προσθέστε αυτόν τον πάροχο RPC μόνο αν είστε σίγουροι ότι μπορείτε να τον εμπιστευτείτε. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Αγοράστε Τώρα" }, - "buyToken": { - "message": "Αγορά $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Επεξεργασία τελών επίσπευσης συναλλαγής" }, - "enable": { - "message": "Ενεργοποίηση" - }, "enableAutoDetect": { "message": " Ενεργοποίηση αυτόματου εντοπισμού" }, @@ -1720,10 +1696,6 @@ "message": "Κωδικός: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Λεπτομέρειες σφάλματος", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Σφάλμα κατά τη λήψη της λίστας ασφαλών αλυσίδων, συνεχίστε με προσοχή." }, @@ -1735,14 +1707,6 @@ "message": "Κωδικός: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Προσπαθήστε ξανά με επαναφόρτωση της σελίδας ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Προσπαθήστε ξανά κλείνοντας και ανοίγοντας ξανά το αναδυόμενο παράθυρο ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Το MetaMask αντιμετώπισε ένα σφάλμα", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Τύπος λειτουργίας" }, - "fundYourWallet": { - "message": "Χρηματοδοτήστε το πορτοφόλι σας" - }, - "fundYourWalletDescription": { - "message": "Ξεκινήστε προσθέτοντας περίπου $1 στο πορτοφόλι σας.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Τέλος συναλλαγής" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Προβολή λογαριασμού σε $1" }, - "getStartedWithNFTs": { - "message": "Λάβετε $1 για να αγοράσετε NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Ξεκινήστε με NFT προσθέτοντας περίπου $1 στο πορτοφόλι σας.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Πηγαίνετε πίσω" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Σύνδεση με" }, - "simulationDetailsFailed": { - "message": "Υπήρξε σφάλμα στη φόρτωση της εκτίμησής σας." - }, "simulationDetailsFiatNotAvailable": { "message": "Μη διαθέσιμο" }, "simulationDetailsIncomingHeading": { "message": "Λαμβάνετε" }, - "simulationDetailsNoBalanceChanges": { - "message": "Δεν προβλέπονται αλλαγές για το πορτοφόλι σας" - }, "simulationDetailsOutgoingHeading": { "message": "Στέλνετε" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Πόροι" }, - "siweSignatureSimulationDetailInfo": { - "message": "Συνδέεστε σε έναν ιστότοπο και δεν προβλέπονται αλλαγές στον λογαριασμό σας." - }, "siweURI": { "message": "Διεύθυνση URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Έξυπνες συναλλαγές" }, - "smartTransactionsBenefit1": { - "message": "Ποσοστό επιτυχίας 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Σας εξοικονομεί χρήματα" - }, - "smartTransactionsBenefit3": { - "message": "Ενημερώσεις σε πραγματικό χρόνο" - }, - "smartTransactionsDescription": { - "message": "Ξεκλειδώστε υψηλότερα ποσοστά επιτυχίας, προστασία σε \"προπορευόμενες συναλλαγές\" και καλύτερη ορατότητα με τις Έξυπνες Συναλλαγές." - }, - "smartTransactionsDescription2": { - "message": "Διατίθεται μόνο στο Ethereum. Ενεργοποιήστε ή απενεργοποιήστε το ανά πάσα στιγμή στις ρυθμίσεις. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Ενισχυμένη Προστασία Συναλλαγών" - }, "snapAccountCreated": { "message": "Ο λογαριασμός δημιουργήθηκε" }, @@ -4858,9 +4779,6 @@ "message": "Επικοινωνήστε με τους διαχειριστές του $1 για περαιτέρω υποστήριξη.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Ορισμένα δίκτυα ενδέχεται να ενέχουν κινδύνους για την ασφάλεια ή/και το απόρρητο. Ενημερωθείτε για τους κινδύνους πριν προσθέσετε και χρησιμοποιήσετε ένα δίκτυο." - }, "somethingDoesntLookRight": { "message": "Κάτι δεν φαίνεται σωστό; $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Stake" }, - "startYourJourney": { - "message": "Ξεκινήστε το ταξίδι σας με $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Ξεκινήστε με Web3 προσθέτοντας περίπου $1 στο πορτοφόλι σας.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Σφάλμα κατά την ανάκτηση αρχείων καταγραφής κατάστασης." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Προβολή δραστηριότητας" }, - "viewAllDetails": { - "message": "Προβολή όλων των λεπτομερειών" - }, "viewAllQuotes": { "message": "προβολή όλων των προσφορών" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1c45a0cd6a6e..56e3614e3f39 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -231,23 +231,6 @@ "addCustomNetwork": { "message": "Add custom network" }, - "addEthereumChainConfirmationDescription": { - "message": "This will allow this network to be used within MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask does not verify custom networks." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Learn about $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "scams and network security risks", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Allow this site to add a network?" - }, "addEthereumChainWarningModalHeader": { "message": "Only add this RPC provider if you’re sure you can trust it. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -514,6 +497,10 @@ "allCustodianAccountsConnectedTitle": { "message": "No accounts available to connect" }, + "allNetworks": { + "message": "All Networks", + "description": "Speicifies to token network filter to filter by all Networks" + }, "allOfYour": { "message": "All of your $1", "description": "$1 is the symbol or name of the token that the user is approving spending" @@ -857,18 +844,40 @@ "bridge": { "message": "Bridge" }, + "bridgeCalculatingAmount": { + "message": "Calculating..." + }, "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeEnterAmount": { + "message": "Enter amount" + }, "bridgeFrom": { "message": "Bridge from" }, + "bridgeOverallCost": { + "message": "Overall cost" + }, "bridgeSelectNetwork": { "message": "Select network" }, + "bridgeSelectTokenAndAmount": { + "message": "Select token and amount" + }, + "bridgeTimingMinutes": { + "message": "$1 minutes", + "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" + }, + "bridgeTimingTooltipText": { + "message": "This is the estimated time it will take for the bridging to be complete." + }, "bridgeTo": { "message": "Bridge to" }, + "bridgeTotalFeesTooltipText": { + "message": "This includes gas fees (paid to crypto miners) and relayer fees (paid to power complex services like bridging).\nFees are based on network traffic and transaction complexity. MetaMask does not profit from either fee." + }, "browserNotSupported": { "message": "Your browser is not supported..." }, @@ -888,12 +897,6 @@ "message": "Buy $1", "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" }, - "buyCrypto": { - "message": "Buy crypto" - }, - "buyFirstCrypto": { - "message": "Buy your first crypto with a debit or credit card." - }, "buyMoreAsset": { "message": "Buy more $1", "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" @@ -901,10 +904,6 @@ "buyNow": { "message": "Buy Now" }, - "buyToken": { - "message": "Buy $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1190,10 +1189,14 @@ "message": "Connected with $1", "description": "$1 represents account name" }, - "connectedWithNetworks": { + "connectedWithNetwork": { "message": "$1 networks connected", "description": "$1 represents network length" }, + "connectedWithNetworkName": { + "message": "Connected with $1", + "description": "$1 represents network name" + }, "connecting": { "message": "Connecting" }, @@ -1368,6 +1371,10 @@ "currentLanguage": { "message": "Current language" }, + "currentNetwork": { + "message": "Current Network", + "description": "Speicifies to token network filter to filter by current Network. Will render when network nickname is not available" + }, "currentRpcUrlDeprecated": { "message": "The current rpc url for this network has been deprecated." }, @@ -1527,6 +1534,9 @@ "dcent": { "message": "D'Cent" }, + "debitCreditPurchaseOptions": { + "message": "Debit or credit card purchase options" + }, "decimal": { "message": "Token decimal" }, @@ -1836,8 +1846,8 @@ "editSpendingCapError": { "message": "The spending cap can’t exceed $1 decimal digits. Remove decimal digits to continue." }, - "enable": { - "message": "Enable" + "editSpendingCapSpecialCharError": { + "message": "Enter numbers only" }, "enableAutoDetect": { "message": " Enable autodetect" @@ -1930,10 +1940,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1945,18 +1951,42 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageContactSupport": { + "message": "Contact support", + "description": "Button for contact MM support" }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageDescribeUsWhatHappened": { + "message": "Describe what happened", + "description": "Button for submitting report to sentry" + }, + "errorPageInfo": { + "message": "Your information can’t be shown. Don’t worry, your wallet and funds are safe.", + "description": "Information banner shown in the error page" + }, + "errorPageMessageTitle": { + "message": "Error message", + "description": "Title for description, which is displayed for debugging purposes" + }, + "errorPageSentryFormTitle": { + "message": "Describe what happened", + "description": "In sentry feedback form, The title at the top of the feedback form." + }, + "errorPageSentryMessagePlaceholder": { + "message": "Sharing details like how we can reproduce the bug will help us fix the problem.", + "description": "In sentry feedback form, The placeholder for the feedback description input field." + }, + "errorPageSentrySuccessMessageText": { + "message": "Thanks! We will take a look soon.", + "description": "In sentry feedback form, The message displayed after a successful feedback submission." }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" }, + "errorPageTryAgain": { + "message": "Try again", + "description": "Button for try again" + }, "errorStack": { "message": "Stack:", "description": "Title for error stack, which is displayed for debugging purposes" @@ -1974,6 +2004,9 @@ "estimatedFeeTooltip": { "message": "Amount paid to process the transaction on network." }, + "estimatedTime": { + "message": "Estimated time" + }, "ethGasPriceFetchWarning": { "message": "Backup gas price is provided as the main gas estimation service is unavailable right now." }, @@ -2119,12 +2152,8 @@ "functionType": { "message": "Function type" }, - "fundYourWallet": { - "message": "Fund your wallet" - }, - "fundYourWalletDescription": { - "message": "Get started by adding some $1 to your wallet.", - "description": "$1 is the token symbol" + "fundingMethod": { + "message": "Funding method" }, "gas": { "message": "Gas" @@ -2215,20 +2244,6 @@ "genericExplorerView": { "message": "View account on $1" }, - "getStarted": { - "message": "Get Started" - }, - "getStartedByFundingWallet": { - "message": "Get started by adding some crypto to your wallet." - }, - "getStartedWithNFTs": { - "message": "Get $1 to buy NFTs", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Get started with NFTs by adding some $1 to your wallet.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Go back" }, @@ -3794,6 +3809,12 @@ "outdatedBrowserNotification": { "message": "Your browser is out of date. If you don't update your browser, you won't be able to get security patches and new features from MetaMask." }, + "overrideContentSecurityPolicyHeader": { + "message": "Override Content-Security-Policy header" + }, + "overrideContentSecurityPolicyHeaderDescription": { + "message": "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility." + }, "padlock": { "message": "Padlock" }, @@ -4465,6 +4486,9 @@ "requestFromInfo": { "message": "This is the site asking for your signature." }, + "requestFromInfoSnap": { + "message": "This is the Snap asking for your signature." + }, "requestFromTransactionDescription": { "message": "This is the site asking for your confirmation." }, @@ -4487,6 +4511,10 @@ "message": "Requesting for $1", "description": "Name of Account" }, + "requestingForNetwork": { + "message": "Requesting for $1", + "description": "Name of Network" + }, "requestsAwaitingAcknowledgement": { "message": "requests waiting to be acknowledged" }, @@ -4674,6 +4702,9 @@ "securityDescription": { "message": "Reduce your chances of joining unsafe networks and protect your accounts" }, + "securityMessageLinkForNetworks": { + "message": "network scams and security risks" + }, "securityPrivacyPath": { "message": "Settings > Security & Privacy." }, @@ -4766,9 +4797,6 @@ "selectEnableDisplayMediaPrivacyPreference": { "message": "Turn on Display NFT Media" }, - "selectFundingMethod": { - "message": "Select a funding method" - }, "selectHdPath": { "message": "Select HD path" }, @@ -4959,17 +4987,14 @@ "simulationDetailsERC20ApproveDesc": { "message": "You're giving someone else permission to spend this amount from your account." }, - "simulationDetailsFailed": { - "message": "There was an error loading your estimation." - }, "simulationDetailsFiatNotAvailable": { "message": "Not Available" }, "simulationDetailsIncomingHeading": { "message": "You receive" }, - "simulationDetailsNoBalanceChanges": { - "message": "No changes predicted for your wallet" + "simulationDetailsNoChanges": { + "message": "No changes" }, "simulationDetailsOutgoingHeading": { "message": "You send" @@ -4993,6 +5018,9 @@ "simulationDetailsTransactionReverted": { "message": "This transaction is likely to fail" }, + "simulationDetailsUnavailable": { + "message": "Unavailable" + }, "simulationErrorMessageV2": { "message": "We were not able to estimate gas. There might be an error in the contract and this transaction may fail." }, @@ -5014,9 +5042,6 @@ "siweResources": { "message": "Resources" }, - "siweSignatureSimulationDetailInfo": { - "message": "You’re signing into a site and there are no predicted changes to your account." - }, "siweURI": { "message": "URL" }, @@ -5059,25 +5084,6 @@ "smartTransactions": { "message": "Smart Transactions" }, - "smartTransactionsBenefit1": { - "message": "99.5% success rate" - }, - "smartTransactionsBenefit2": { - "message": "Saves you money" - }, - "smartTransactionsBenefit3": { - "message": "Real-time updates" - }, - "smartTransactionsDescription": { - "message": "Unlock higher success rates, frontrunning protection, and better visibility with Smart Transactions." - }, - "smartTransactionsDescription2": { - "message": "Only available on Ethereum. Enable or disable any time in settings. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Enhanced Transaction Protection" - }, "snapAccountCreated": { "message": "Account created" }, @@ -5244,8 +5250,14 @@ "message": "Contact the creators of $1 for further support.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network." + "solanaSupportSectionTitle": { + "message": "Solana" + }, + "solanaSupportToggleDescription": { + "message": "Turning on this feature will give you the option to add a Solana Account to your MetaMask Extension derived from your existing Secret Recovery Phrase. This is an experimental Beta feature, so you should use it at your own risk." + }, + "solanaSupportToggleTitle": { + "message": "Enable \"Add a new Solana account (Beta)\"" }, "somethingDoesntLookRight": { "message": "Something doesn't look right? $1", @@ -5419,14 +5431,6 @@ "stake": { "message": "Stake" }, - "startYourJourney": { - "message": "Start your journey with $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Get started with web3 by adding some $1 to your wallet.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Error in retrieving state logs." }, @@ -5913,6 +5917,10 @@ "swapTokenVerifiedOn1SourceTitle": { "message": "Potentially inauthentic token" }, + "swapTokenVerifiedSources": { + "message": "Confirmed by $1 sources. Verify on $2.", + "description": "$1 the number of sources that have verified the token, $2 points the user to a block explorer as a place they can verify information about the token." + }, "swapTooManyDecimalsError": { "message": "$1 allows up to $2 decimals", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -6030,6 +6038,12 @@ "tips": { "message": "Tips" }, + "tipsForUsingAWallet": { + "message": "Tips for using a wallet" + }, + "tipsForUsingAWalletDescription": { + "message": "Adding tokens unlocks more ways to use web3." + }, "to": { "message": "To" }, @@ -6082,6 +6096,9 @@ "tokenList": { "message": "Token lists" }, + "tokenMarketplace": { + "message": "Token marketplace" + }, "tokenScamSecurityRisk": { "message": "token scams and security risks" }, @@ -6119,6 +6136,9 @@ "total": { "message": "Total" }, + "totalFees": { + "message": "Total fees" + }, "totalVolume": { "message": "Total volume" }, @@ -6330,9 +6350,6 @@ "unknown": { "message": "Unknown" }, - "unknownChainWarning": { - "message": "We can’t verify custom networks. To avoid malicious providers from recording your network activity, only add networks you trust." - }, "unknownCollection": { "message": "Unnamed collection" }, @@ -6461,9 +6478,6 @@ "viewActivity": { "message": "View activity" }, - "viewAllDetails": { - "message": "View all details" - }, "viewAllQuotes": { "message": "view all quotes" }, @@ -6546,6 +6560,10 @@ "watchEthereumAccountsToggle": { "message": "Watch Ethereum Accounts (Beta)" }, + "watchOutMessage": { + "message": "Beware of $1.", + "description": "$1 is a link with text that is provided by the 'securityMessageLinkForNetworks' key" + }, "weak": { "message": "Weak" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index fc635e33a708..9ee7771947ba 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1707,9 +1707,6 @@ "effortlesslyNavigateYourDigitalAssets": { "message": "Effortlessly navigate your digital assets" }, - "enable": { - "message": "Enable" - }, "enableAutoDetect": { "message": " Enable autodetect" }, @@ -1795,10 +1792,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1810,14 +1803,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" @@ -4714,18 +4699,12 @@ "signingInWith": { "message": "Signing in with" }, - "simulationDetailsFailed": { - "message": "There was an error loading your estimation." - }, "simulationDetailsFiatNotAvailable": { "message": "Not Available" }, "simulationDetailsIncomingHeading": { "message": "You receive" }, - "simulationDetailsNoBalanceChanges": { - "message": "No changes predicted for your wallet" - }, "simulationDetailsOutgoingHeading": { "message": "You send" }, @@ -4763,9 +4742,6 @@ "siweResources": { "message": "Resources" }, - "siweSignatureSimulationDetailInfo": { - "message": "You’re signing into a site and there are no predicted changes to your account." - }, "siweURI": { "message": "URL" }, @@ -4815,25 +4791,6 @@ "smartTransactions": { "message": "Smart Transactions" }, - "smartTransactionsBenefit1": { - "message": "99.5% success rate" - }, - "smartTransactionsBenefit2": { - "message": "Saves you money" - }, - "smartTransactionsBenefit3": { - "message": "Real-time updates" - }, - "smartTransactionsDescription": { - "message": "Unlock higher success rates, frontrunning protection, and better visibility with Smart Transactions." - }, - "smartTransactionsDescription2": { - "message": "Only available on Ethereum. Enable or disable any time in settings. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Enhanced Transaction Protection" - }, "snapAccountCreated": { "message": "Account created" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 97d6f4be9854..f13313976e74 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Agregar red personalizada" }, - "addEthereumChainConfirmationDescription": { - "message": "Esto permitirá que la red se utilice en MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask no verifica redes personalizadas." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Obtenga más información sobre $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "estafas y riesgos de seguridad de la red", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "¿Permitir que este sitio agregue una red?" - }, "addEthereumChainWarningModalHeader": { "message": "Agregue este proveedor de RPC solo si está seguro de que puede confiar en él. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -819,10 +802,6 @@ "buyNow": { "message": "Comprar ahora" }, - "buyToken": { - "message": "Comprar $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1638,9 +1617,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Editar la tarifa de aceleración de gas" }, - "enable": { - "message": "Habilitar" - }, "enableAutoDetect": { "message": " Activar autodetección" }, @@ -1717,10 +1693,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error al obtener la lista de cadenas seguras, por favor continúe con precaución." }, @@ -1732,14 +1704,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" @@ -1897,13 +1861,6 @@ "functionType": { "message": "Tipo de función" }, - "fundYourWallet": { - "message": "Agregar fondos a su monedero" - }, - "fundYourWalletDescription": { - "message": "Comience agregando $1 a su monedero.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gas" }, @@ -1987,14 +1944,6 @@ "genericExplorerView": { "message": "Ver cuenta en $1" }, - "getStartedWithNFTs": { - "message": "Obtenga $1 para comprar NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Comience con los NFT agregando $1 a su monedero.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Volver" }, @@ -4576,18 +4525,12 @@ "signingInWith": { "message": "Iniciar sesión con" }, - "simulationDetailsFailed": { - "message": "Se produjo un error al cargar su estimación." - }, "simulationDetailsFiatNotAvailable": { "message": "No disponible" }, "simulationDetailsIncomingHeading": { "message": "Usted recibe" }, - "simulationDetailsNoBalanceChanges": { - "message": "No se prevén cambios para su monedero" - }, "simulationDetailsOutgoingHeading": { "message": "Envía" }, @@ -4625,9 +4568,6 @@ "siweResources": { "message": "Recursos" }, - "siweSignatureSimulationDetailInfo": { - "message": "Está iniciando sesión en un sitio y no se prevén cambios en su cuenta." - }, "siweURI": { "message": "URL" }, @@ -4670,25 +4610,6 @@ "smartTransactions": { "message": "Transacciones inteligentes" }, - "smartTransactionsBenefit1": { - "message": "Índice de éxito del 99.5%" - }, - "smartTransactionsBenefit2": { - "message": "Le permite ahorrar dinero" - }, - "smartTransactionsBenefit3": { - "message": "Actualizaciones en tiempo real" - }, - "smartTransactionsDescription": { - "message": "Desbloquee índices de éxito más altos, protección contra frontrunning y mejor visibilidad con transacciones inteligentes." - }, - "smartTransactionsDescription2": { - "message": "Solo disponible en Ethereum. Active o desactive en cualquier momento en la configuración. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Protección mejorada de transacciones" - }, "snapAccountCreated": { "message": "Cuenta creada" }, @@ -4855,9 +4776,6 @@ "message": "Póngase en contacto con los creadores de $1 para obtener más ayuda.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Algunas redes pueden presentar riesgos de seguridad y/o privacidad. Comprenda los riesgos antes de agregar y utilizar una red." - }, "somethingDoesntLookRight": { "message": "Algo no se ve bien, ¿cierto? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5011,14 +4929,6 @@ "stake": { "message": "Staking" }, - "startYourJourney": { - "message": "Comience su recorrido con $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Comience con la web3 agregando $1 a su monedero.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Error al recuperar los registros de estado." }, @@ -6004,9 +5914,6 @@ "viewActivity": { "message": "Ver actividad" }, - "viewAllDetails": { - "message": "Ver todos los detalles" - }, "viewAllQuotes": { "message": "ver todas las cotizaciones" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 672823c370ba..511ee6cbef71 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -100,23 +100,6 @@ "addContact": { "message": "Agregar contacto" }, - "addEthereumChainConfirmationDescription": { - "message": "Esto permitirá que la red se utilice en MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask no verifica redes personalizadas." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Obtenga más información sobre $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "estafas y riesgos de seguridad de la red", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "¿Permitir que este sitio agregue una red?" - }, "addFriendsAndAddresses": { "message": "Agregue amigos y direcciones de confianza" }, @@ -703,10 +686,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensaje: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -715,14 +694,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" @@ -2335,9 +2306,6 @@ "userName": { "message": "Nombre de usuario" }, - "viewAllDetails": { - "message": "Ver todos los detalles" - }, "viewContact": { "message": "Ver contacto" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index dbaffd44cf38..c785d2e0a00a 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Ajouter un réseau personnalisé" }, - "addEthereumChainConfirmationDescription": { - "message": "Cela permettra d’utiliser ce réseau dans MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask ne vérifie pas les réseaux personnalisés." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "En savoir plus sur $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "les risques de fraude et de sécurité des réseaux", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Autoriser ce site à ajouter un réseau ?" - }, "addEthereumChainWarningModalHeader": { "message": "N’ajoutez ce fournisseur de RPC que si vous lui faites confiance. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Achetez maintenant" }, - "buyToken": { - "message": "Acheter des $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Octets" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Modifier les gas fees d’accélération" }, - "enable": { - "message": "Activer" - }, "enableAutoDetect": { "message": " Activer la détection automatique" }, @@ -1720,10 +1696,6 @@ "message": "Code : $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Détails de l’erreur", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erreur lors de l’obtention de la liste des chaînes sécurisées, veuillez continuer avec précaution." }, @@ -1735,14 +1707,6 @@ "message": "Code : $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Essayez à nouveau en rechargeant la page, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Réessayez en fermant puis en rouvrant le pop-up, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask a rencontré une erreur", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Type de fonction" }, - "fundYourWallet": { - "message": "Approvisionnez votre portefeuille" - }, - "fundYourWalletDescription": { - "message": "Commencez par ajouter quelques $1 à votre portefeuille.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Carburant" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Voir le compte sur $1" }, - "getStartedWithNFTs": { - "message": "Obtenez des $1 pour acheter des NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Débutez avec les NFT en ajoutant quelques $1 à votre portefeuille.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Retour" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Se connecter avec" }, - "simulationDetailsFailed": { - "message": "Une erreur s’est produite lors du chargement de l’estimation." - }, "simulationDetailsFiatNotAvailable": { "message": "Non disponible" }, "simulationDetailsIncomingHeading": { "message": "Vous recevez" }, - "simulationDetailsNoBalanceChanges": { - "message": "Aucun changement prévu pour votre portefeuille" - }, "simulationDetailsOutgoingHeading": { "message": "Vous envoyez" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Ressources" }, - "siweSignatureSimulationDetailInfo": { - "message": "Vous êtes en train de vous connecter à un site, aucun changement ne devrait être apporté à votre compte." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Transactions intelligentes" }, - "smartTransactionsBenefit1": { - "message": "Taux de réussite de 99,5 %" - }, - "smartTransactionsBenefit2": { - "message": "Cela vous permet d’économiser de l’argent" - }, - "smartTransactionsBenefit3": { - "message": "Mises à jour en temps réel" - }, - "smartTransactionsDescription": { - "message": "Bénéficiez de taux de réussite plus élevés, d’une protection contre le « front running » et d’une meilleure visibilité grâce aux transactions intelligentes." - }, - "smartTransactionsDescription2": { - "message": "Disponible uniquement sur Ethereum. Vous pouvez activer ou désactiver cette option à tout moment dans les paramètres. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Protection renforcée des transactions" - }, "snapAccountCreated": { "message": "Le compte a été créé" }, @@ -4858,9 +4779,6 @@ "message": "L’interface utilisateur (IU) spécifiée par le snap n’est pas valide.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Certains réseaux peuvent présenter des risques pour la sécurité et/ou la vie privée. Informez-vous sur les risques avant d’ajouter et d’utiliser un réseau." - }, "somethingDoesntLookRight": { "message": "On dirait que quelque chose ne va pas ? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Staker" }, - "startYourJourney": { - "message": "Lancez-vous dans les $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Lancez-vous dans le Web3 en ajoutant quelques $1 à votre portefeuille.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Erreur lors du chargement des journaux d’état." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Voir l’activité" }, - "viewAllDetails": { - "message": "Afficher tous les détails" - }, "viewAllQuotes": { "message": "afficher toutes les cotations" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 0e624b4ba807..554bb2e91b84 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "कस्टम नेटवर्क जोड़ें" }, - "addEthereumChainConfirmationDescription": { - "message": "इससे इस नेटवर्क को MetaMask के अंदर इस्तेमाल करने की अनुमति मिलेगी।" - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask कस्टम नेटवर्क को वेरीफ़ाई नहीं करता है।" - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "$1 के बारे में जानें।", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "स्कैम और नेटवर्क से जुड़े सुरक्षा जोखिम", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "इस साइट को नेटवर्क जोड़ने की अनुमति दें?" - }, "addEthereumChainWarningModalHeader": { "message": "इस RPC प्रोवाइडर को केवल तभी जोड़ें जब आप निश्चित हैं कि आप इस पर विश्वास कर सकते हैं। $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "अभी खरीदें" }, - "buyToken": { - "message": "$1 खरीदें", - "description": "$1 is the token symbol" - }, "bytes": { "message": "बाइट" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "गैस फ़ीस स्पीड अप को बदलें" }, - "enable": { - "message": "चालू करें" - }, "enableAutoDetect": { "message": " ऑटो डिटेक्ट इनेबल करें" }, @@ -1720,10 +1696,6 @@ "message": "कोड: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "गड़बड़ी की जानकारी", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "सेफ चेन लिस्ट पाते समय गड़बड़ी हुई, कृपया सावधानी के साथ जारी रखें।" }, @@ -1735,14 +1707,6 @@ "message": "कोड: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "पेज को दोबारा लोड करके फिर से कोशिश करें या सपोर्ट $1 से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "पॉपअप को बंद करके और फिर से खोलने की कोशिश करें या $1 पर सपोर्ट से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask में कोई गड़बड़ी हुई", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "फ़ंक्शन का प्रकार" }, - "fundYourWallet": { - "message": "अपने वॉलेट को फंड करें" - }, - "fundYourWalletDescription": { - "message": "अपने वॉलेट में कुछ $1 जोड़कर शुरुआत करें।", - "description": "$1 is the token symbol" - }, "gas": { "message": "गैस" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "$1 पर अकाउंट देखें" }, - "getStartedWithNFTs": { - "message": "NFTs खरीदने के लिए $1 प्राप्त करें", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "अपने वॉलेट में कुछ $1 जोड़कर NFTs से शुरुआत करें।", - "description": "$1 is the token symbol" - }, "goBack": { "message": "वापस जाएं" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "के साथ साइन इन करना" }, - "simulationDetailsFailed": { - "message": "आपका एस्टीमेशन लोड करने में गड़बड़ी हुई।" - }, "simulationDetailsFiatNotAvailable": { "message": "उपलब्ध नहीं है" }, "simulationDetailsIncomingHeading": { "message": "आप पाते हैं" }, - "simulationDetailsNoBalanceChanges": { - "message": "आपके वॉलेट के लिए किसी बदलाव का प्रेडिक्शन नहीं किया गया है" - }, "simulationDetailsOutgoingHeading": { "message": "आप भेजते हैं" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "संसाधन" }, - "siweSignatureSimulationDetailInfo": { - "message": "आप किसी साइट पर साइन इन कर रहे हैं और आपके अकाउंट में कोई अनुमानित परिवर्तन नहीं हैं।" - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "स्मार्ट ट्रांसेक्शन" }, - "smartTransactionsBenefit1": { - "message": "99.5% सफलता दर" - }, - "smartTransactionsBenefit2": { - "message": "आपका पैसा बचाता है" - }, - "smartTransactionsBenefit3": { - "message": "रियल-टाइम अपडेट" - }, - "smartTransactionsDescription": { - "message": "स्मार्ट ट्रांसेक्शन के साथ उच्च सफलता दर, फ्रंटरनिंग सुरक्षा और बेहतर दृश्यता अनलॉक करें।" - }, - "smartTransactionsDescription2": { - "message": "केवल Ethereum पर उपलब्ध है। सेटिंग्स में किसी भी समय चालू करें या बंद करें। $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "एनहांस्ड ट्रांसेक्शन प्रोटेक्शन" - }, "snapAccountCreated": { "message": "अकाउंट बनाया गया" }, @@ -4858,9 +4779,6 @@ "message": "अधिक सहायता के लिए $1 के निर्माताओं से कॉन्टेक्ट करें।", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "कुछ नेटवर्क सुरक्षा और/या गोपनीयता संबंधी जोखिम पैदा कर सकते हैं। नेटवर्क जोड़ने और इस्तेमाल करने से पहले जोखिमों को समझें।" - }, "somethingDoesntLookRight": { "message": "कुछ तो गड़बड़ है? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "हिस्सेदारी" }, - "startYourJourney": { - "message": "$1 से अपनी यात्रा शुरू करें", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "अपने वॉलेट में कुछ $1 जोड़कर Web3 से शुरुआत करें।", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "स्टेट लॉग को पुनर्प्राप्त करने में गड़बड़ी।" }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "एक्टिविटी देखें" }, - "viewAllDetails": { - "message": "सभी विवरण देखें" - }, "viewAllQuotes": { "message": "सभी उद्धरण को देखें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 68b52556c3f6..8e60e6fe5a50 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Tambahkan jaringan khusus" }, - "addEthereumChainConfirmationDescription": { - "message": "Tindakan ini akan membantu jaringan ini agar dapat digunakan di MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask tidak memverifikasi jaringan kustom." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Pelajari tentang $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "penipuan dan risiko keamanan jaringan", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Izinkan situs ini untuk menambahkan jaringan?" - }, "addEthereumChainWarningModalHeader": { "message": "Cukup tambahkan penyedia RPC ini jika Anda yakin dapat memercayainya. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Beli Sekarang" }, - "buyToken": { - "message": "Beli $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Byte" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Edit biaya gas percepatan" }, - "enable": { - "message": "Aktifkan" - }, "enableAutoDetect": { "message": " Aktifkan deteksi otomatis" }, @@ -1720,10 +1696,6 @@ "message": "Kode: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detail kesalahan", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Terjadi kesalahan saat mendapatkan daftar rantai aman, lanjutkan dengan hati-hati." }, @@ -1735,14 +1707,6 @@ "message": "Kode: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Coba lagi dengan memuat kembali halaman, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Coba lagi dengan menutup dan membuka kembali sembulan, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask mengalami kesalahan", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Jenis fungsi" }, - "fundYourWallet": { - "message": "Danai dompet Anda" - }, - "fundYourWalletDescription": { - "message": "Mulailah dengan menambahkan sejumlah $1 ke dompet Anda.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gas" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Lihat akun di $1" }, - "getStartedWithNFTs": { - "message": "Dapatkan $1 untuk membeli NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Mulailah menggunakan NFT dengan menambahkan sejumlah $1 ke dompet Anda.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Kembali" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Masuk dengan" }, - "simulationDetailsFailed": { - "message": "Terjadi kesalahan saat memuat estimasi Anda." - }, "simulationDetailsFiatNotAvailable": { "message": "Tidak Tersedia" }, "simulationDetailsIncomingHeading": { "message": "Anda menerima" }, - "simulationDetailsNoBalanceChanges": { - "message": "Tidak ada perubahan yang terprediksi untuk dompet Anda" - }, "simulationDetailsOutgoingHeading": { "message": "Anda mengirim" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Sumber daya" }, - "siweSignatureSimulationDetailInfo": { - "message": "Anda masuk ke sebuah situs dan tidak ada perkiraan perubahan pada akun Anda." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Transaksi Pintar" }, - "smartTransactionsBenefit1": { - "message": "Tingkat keberhasilan 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Menghemat uang Anda" - }, - "smartTransactionsBenefit3": { - "message": "Pembaruan waktu nyata" - }, - "smartTransactionsDescription": { - "message": "Raih tingkat keberhasilan yang lebih tinggi, perlindungan frontrunning, dan visibilitas yang lebih baik dengan Transaksi Pintar." - }, - "smartTransactionsDescription2": { - "message": "Hanya tersedia di Ethereum. Aktifkan atau nonaktifkan kapan saja di pengaturan. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Peningkatan Perlindungan Transaksi" - }, "snapAccountCreated": { "message": "Akun dibuat" }, @@ -4858,9 +4779,6 @@ "message": "Hubungi pembuat $1 untuk dukungan lebih lanjut.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Beberapa jaringan dapat menimbulkan risiko keamanan dan/atau privasi. Pahami risikonya sebelum menambahkan dan menggunakan jaringan." - }, "somethingDoesntLookRight": { "message": "Ada yang tidak beres? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Stake" }, - "startYourJourney": { - "message": "Mulailah perjalanan Anda dengan $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Mulailah dengan web3 dengan menambahkan sejumlah $1 ke dompet Anda.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Terjadi kesalahan pada log status pengambilan." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Lihat aktivitas" }, - "viewAllDetails": { - "message": "Lihat semua detail" - }, "viewAllQuotes": { "message": "lihat semua kuotasi" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 4dac80c253b3..05cccdac0359 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -152,23 +152,6 @@ "addContact": { "message": "Aggiungi contatto" }, - "addEthereumChainConfirmationDescription": { - "message": "Ciò consentirà a questa rete di essere utilizzata all'interno di MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask non verifica le reti personalizzate." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Maggiori informazioni su $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "truffe e rischi per la sicurezza della rete", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Consenti a questo sito di aggiungere una rete?" - }, "addFriendsAndAddresses": { "message": "Aggiungi amici e indirizzi di cui ti fidi" }, @@ -753,10 +736,6 @@ "message": "Codice: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Dettagli Errore", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Messaggio: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -1649,9 +1628,6 @@ "userName": { "message": "Nome utente" }, - "viewAllDetails": { - "message": "Vedi tutti i dettagli" - }, "viewContact": { "message": "Visualizza contatto" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 73f3f8300646..69824ae33b52 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "カスタムネットワークを追加" }, - "addEthereumChainConfirmationDescription": { - "message": "これにより、このネットワークはMetaMask内で使用できるようになります。" - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMaskはカスタムネットワークを検証しません。" - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "$1の詳細。", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "詐欺やネットワークセキュリティのリスク", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "このサイトにネットワークの追加を許可しますか?" - }, "addEthereumChainWarningModalHeader": { "message": "このRPCプロバイダーは、確実に信頼できる場合のみ追加してください。$1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "今すぐ購入" }, - "buyToken": { - "message": "$1を購入", - "description": "$1 is the token symbol" - }, "bytes": { "message": "バイト" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "高速化用のガス代を編集" }, - "enable": { - "message": "有効にする" - }, "enableAutoDetect": { "message": " 自動検出を有効にする" }, @@ -1720,10 +1696,6 @@ "message": "コード: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "エラーの詳細", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "安全なチェーンリストの取得中にエラーが発生しました。慎重に続けてください。" }, @@ -1735,14 +1707,6 @@ "message": "コード: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "ページを再ロードしてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "ポップアップを閉じてから再び開いてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMaskにエラーが発生しました", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "機能の種類" }, - "fundYourWallet": { - "message": "ウォレットへの入金" - }, - "fundYourWalletDescription": { - "message": "ウォレットに$1を追加して開始します。", - "description": "$1 is the token symbol" - }, "gas": { "message": "ガス" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "$1でアカウントを表示" }, - "getStartedWithNFTs": { - "message": "$1を入手してNFTを購入", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "ウォレットに$1を追加してNFTの利用を開始します。", - "description": "$1 is the token symbol" - }, "goBack": { "message": "戻る" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "サインイン方法:" }, - "simulationDetailsFailed": { - "message": "予測結果の読み込み中にエラーが発生しました。" - }, "simulationDetailsFiatNotAvailable": { "message": "利用できません" }, "simulationDetailsIncomingHeading": { "message": "受取額" }, - "simulationDetailsNoBalanceChanges": { - "message": "ウォレット残高の増減は予測されていません" - }, "simulationDetailsOutgoingHeading": { "message": "送金額" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "リソース" }, - "siweSignatureSimulationDetailInfo": { - "message": "サイトにサインインしようとしていて、予想されるアカウントの変更はありません。" - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "スマートトランザクション" }, - "smartTransactionsBenefit1": { - "message": "99.5%の成功率" - }, - "smartTransactionsBenefit2": { - "message": "お金を節約できます" - }, - "smartTransactionsBenefit3": { - "message": "リアルタイムの最新情報" - }, - "smartTransactionsDescription": { - "message": "スマートトランザクションで、成功率を上げ、フロントランニングを防ぎ、可視性を高めましょう。" - }, - "smartTransactionsDescription2": { - "message": "イーサリアムでのみご利用いただけ、いつでも設定で有効・無効を切り替えられます。$1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "強化されたトランザクション保護" - }, "snapAccountCreated": { "message": "アカウントが作成されました" }, @@ -4858,9 +4779,6 @@ "message": "今後のサポートは、$1の作成者にお問い合わせください。", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "ネットワークによっては、セキュリティやプライバシーの面でリスクが伴う可能性があります。ネットワークを追加・使用する前にリスクを理解するようにしてください。" - }, "somethingDoesntLookRight": { "message": "何か不審な点があれば、$1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "ステーク" }, - "startYourJourney": { - "message": "$1で利用開始", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "ウォレットに$1を追加してWeb3の利用を開始します。", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "ステートログの取得中にエラーが発生しました。" }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "アクティビティを表示" }, - "viewAllDetails": { - "message": "すべての詳細の表示" - }, "viewAllQuotes": { "message": "すべてのクォートを表示" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index be1de55c51c7..f160b3dccbc2 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "맞춤 네트워크 추가" }, - "addEthereumChainConfirmationDescription": { - "message": "이렇게 하면 MetaMask 내에서 이 네트워크를 사용할 수 있습니다." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask는 맞춤 네트워크를 검증하지 않습니다." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "$1에 대해 알아보기", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "사기 및 네트워크 보안 위험", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "이 사이트에서 네트워크를 추가하도록 허용하시겠습니까?" - }, "addEthereumChainWarningModalHeader": { "message": "신뢰할 수 있는 경우에만 이 RPC 공급입체를 추가하세요. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "지금 구매" }, - "buyToken": { - "message": "$1 구매", - "description": "$1 is the token symbol" - }, "bytes": { "message": "바이트" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "가스비 가속 편집" }, - "enable": { - "message": "활성화" - }, "enableAutoDetect": { "message": " 자동 감지 활성화" }, @@ -1720,10 +1696,6 @@ "message": "코드: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "오류 세부 정보", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "안전 체인 목록을 가져오는 동안 오류가 발생했습니다. 주의하여 계속 진행하세요." }, @@ -1735,14 +1707,6 @@ "message": "코드: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "페이지를 새로고침하여 다시 시도하거나 $1로 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "팝업을 닫은 후 다시 열어 다시 시도하거나 $1에서 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 오류 발생", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "기능 유형" }, - "fundYourWallet": { - "message": "지갑에 자금 추가" - }, - "fundYourWalletDescription": { - "message": "지갑에 $1의 자금을 추가하여 시작하세요.", - "description": "$1 is the token symbol" - }, "gas": { "message": "가스" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "$1에서 계정 보기" }, - "getStartedWithNFTs": { - "message": "$1 받고 NFT 구매하기", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "지갑에 $1의 자금을 추가하여 시작하세요.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "뒤로 가기" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "다음으로 로그인:" }, - "simulationDetailsFailed": { - "message": "추정치를 불러오는 동안 오류가 발생했습니다." - }, "simulationDetailsFiatNotAvailable": { "message": "이용할 수 없음" }, "simulationDetailsIncomingHeading": { "message": "받음:" }, - "simulationDetailsNoBalanceChanges": { - "message": "지갑에 예상 변동 사항 없음" - }, "simulationDetailsOutgoingHeading": { "message": "보냄:" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "리소스" }, - "siweSignatureSimulationDetailInfo": { - "message": "사이트에 로그인 중이며 계정에 예상되는 변경 사항이 없습니다." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "스마트 트랜잭션" }, - "smartTransactionsBenefit1": { - "message": "99.5% 성공률" - }, - "smartTransactionsBenefit2": { - "message": "비용 절감" - }, - "smartTransactionsBenefit3": { - "message": "실시간 업데이트" - }, - "smartTransactionsDescription": { - "message": "스마트 트랜잭션으로 선행거래를 방지하고 더 높은 성공률과 가시성을 확보하세요." - }, - "smartTransactionsDescription2": { - "message": "이더리움에서만 사용할 수 있습니다. 설정에서 언제든지 활성화하거나 비활성화할 수 있습니다. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "트랜잭션 보호 강화" - }, "snapAccountCreated": { "message": "계정 생성됨" }, @@ -4858,9 +4779,6 @@ "message": "$1 작성자에게 연락하여 향후 지원을 요청하세요.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "네트워크에 따라 보안이나 개인 정보 유출의 위험이 있을 수 있습니다. 네트워크 추가 및 사용 이전에 위험 요소를 파악하세요." - }, "somethingDoesntLookRight": { "message": "무언가 잘못되었나요? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "스테이크" }, - "startYourJourney": { - "message": "$1 토큰으로 여정을 시작하세요", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "지갑에 $1 토큰을 추가하여 웹3를 시작하세요.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "상태 로그를 가져오는 도중 오류가 발생했습니다." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "활동 보기" }, - "viewAllDetails": { - "message": "모든 세부 정보 보기" - }, "viewAllQuotes": { "message": "모든 견적 보기" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 1687cb7818f0..3c08d76cb186 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -42,23 +42,6 @@ "addContact": { "message": "Magdagdag ng contact" }, - "addEthereumChainConfirmationDescription": { - "message": "Bibigyang-daan nito na magamit ang network na ito sa MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "Hindi vine-verify ng MetaMask ang mga custom na network." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Matuto tungkol sa $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "mga scam at panganib sa seguridad ng network", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Payagan ang site na ito na magdagdag ng network?" - }, "addFriendsAndAddresses": { "message": "Magdagdag ng mga kaibigan at address na pinagkakatiwalaan mo" }, @@ -440,10 +423,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensahe: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -452,14 +431,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukan ulit sa pamamagitan ng pag-reload ng page, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan ulit sa pamamagitan ng pagsara at pagbukas ulit ng popup, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" @@ -1594,9 +1565,6 @@ "userName": { "message": "Username" }, - "viewAllDetails": { - "message": "Tingnan ang lahat ng detalye" - }, "viewContact": { "message": "Tingnan ang Contact" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 656733cecf65..589a58e94907 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Adicionar rede personalizada" }, - "addEthereumChainConfirmationDescription": { - "message": "Isso permitirá que essa rede seja usada dentro da MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "A MetaMask não verifica redes personalizadas." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Saiba mais sobre $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "golpes e riscos de segurança nas redes", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Permitir que este site adicione uma rede?" - }, "addEthereumChainWarningModalHeader": { "message": "Adicione esse provedor de RPC apenas se tiver certeza de que é confiável. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Comprar agora" }, - "buyToken": { - "message": "Comprar $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Editar taxa de gás para aceleração" }, - "enable": { - "message": "Ativar" - }, "enableAutoDetect": { "message": " Ativar detecção automática" }, @@ -1720,10 +1696,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erro ao obter uma lista segura da cadeia. Por favor, prossiga com cautela." }, @@ -1735,14 +1707,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Tipo de função" }, - "fundYourWallet": { - "message": "Adicione valores à sua carteira" - }, - "fundYourWalletDescription": { - "message": "Comece adicionando $1 à sua carteira.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gás" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Ver conta na $1" }, - "getStartedWithNFTs": { - "message": "Adquira $1 para comprar NFTs", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Comece sua jornada com NFTs adicionando $1 à sua carteira.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Voltar" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Assinando com" }, - "simulationDetailsFailed": { - "message": "Houve um erro ao carregar sua estimativa." - }, "simulationDetailsFiatNotAvailable": { "message": "Não disponível" }, "simulationDetailsIncomingHeading": { "message": "Você recebe" }, - "simulationDetailsNoBalanceChanges": { - "message": "Nenhuma alteração prevista para sua carteira" - }, "simulationDetailsOutgoingHeading": { "message": "Você envia" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Recursos" }, - "siweSignatureSimulationDetailInfo": { - "message": "Você está fazendo login em um site e não há alterações previstas em sua conta." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Transações inteligentes" }, - "smartTransactionsBenefit1": { - "message": "99,5% de taxa de sucesso" - }, - "smartTransactionsBenefit2": { - "message": "Faz você economizar dinheiro" - }, - "smartTransactionsBenefit3": { - "message": "Atualizações em tempo real" - }, - "smartTransactionsDescription": { - "message": "Desbloqueie taxas de sucesso maiores, proteção contra front running e melhor visibilidade com as transações inteligentes." - }, - "smartTransactionsDescription2": { - "message": "Disponível somente na Ethereum. Ative ou desative a qualquer momento nas configurações. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Proteção de transações aprimorada" - }, "snapAccountCreated": { "message": "Conta criada" }, @@ -4858,9 +4779,6 @@ "message": "Contate os criadores de $1 para receber mais suporte.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Algumas redes podem representar riscos de segurança e/ou privacidade. Tenha os riscos em mente antes de adicionar e usar uma rede." - }, "somethingDoesntLookRight": { "message": "Alguma coisa não parece certa? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Stake" }, - "startYourJourney": { - "message": "Comece sua jornada com $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Comece sua jornada na web3 adicionando $1 à sua conta.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Erro ao recuperar os logs de estado." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Ver atividade" }, - "viewAllDetails": { - "message": "Ver todos os detalhes" - }, "viewAllQuotes": { "message": "ver todas as cotações" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 6062013d6d6f..ebf2af88c186 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -100,23 +100,6 @@ "addContact": { "message": "Adicionar contato" }, - "addEthereumChainConfirmationDescription": { - "message": "Isso permitirá que essa rede seja usada dentro da MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "A MetaMask não verifica redes personalizadas." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Saiba mais sobre $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "fraudes e riscos de segurança da rede", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Permitir que esse site adicione uma rede?" - }, "addFriendsAndAddresses": { "message": "Adicionar amigos e endereços confiáveis" }, @@ -703,10 +686,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensagem: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -715,14 +694,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" @@ -2335,9 +2306,6 @@ "userName": { "message": "Nome de usuário" }, - "viewAllDetails": { - "message": "Ver todos os detalhes" - }, "viewContact": { "message": "Ver contato" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 0c2f92821ed2..9462d9c6eb4a 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Добавить пользовательскую сеть" }, - "addEthereumChainConfirmationDescription": { - "message": "Это позволит использовать эту сеть в MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask не проверяет пользовательские сети." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Подробнее о $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "мошенничестве и угрозах безопасности в сети", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Разрешить этому сайту добавить сеть?" - }, "addEthereumChainWarningModalHeader": { "message": "Добавляйте этого поставщика RPC только в том случае, если уверены, что ему можно доверять. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Купить сейчас" }, - "buyToken": { - "message": "Купить $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Байты" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Изменить плату за газ за ускорение" }, - "enable": { - "message": "Включить" - }, "enableAutoDetect": { "message": " Включить автоопределение" }, @@ -1720,10 +1696,6 @@ "message": "Код: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Сведения об ошибке", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Ошибка при получении списка безопасных блокчейнов. Продолжайте с осторожностью." }, @@ -1735,14 +1707,6 @@ "message": "Код: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Повторите попытку, перезагрузив страницу, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Повторите попытку, закрыв и вновь открыв всплывающее окно, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask обнаружил ошибку", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Тип функции" }, - "fundYourWallet": { - "message": "Пополните свой кошелек" - }, - "fundYourWalletDescription": { - "message": "Начните с добавления $1 в свой кошелек.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Газ" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Посмотреть счет на $1" }, - "getStartedWithNFTs": { - "message": "Получите $1 для покупки NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Начните использовать NFT, добавив $1 в свой кошелек.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Назад" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Вход с помощью" }, - "simulationDetailsFailed": { - "message": "Не удалось загрузить прогноз." - }, "simulationDetailsFiatNotAvailable": { "message": "Недоступно" }, "simulationDetailsIncomingHeading": { "message": "Вы получаете" }, - "simulationDetailsNoBalanceChanges": { - "message": "Никаких изменений в вашем кошельке не прогнозируется" - }, "simulationDetailsOutgoingHeading": { "message": "Вы отправляете" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Ресурсы" }, - "siweSignatureSimulationDetailInfo": { - "message": "Вы входите на сайт, и в вашем счете не происходит никаких прогнозируемых изменений." - }, "siweURI": { "message": "URL-адрес" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Умные транзакции" }, - "smartTransactionsBenefit1": { - "message": "Коэффициент успеха 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Экономит вам деньги" - }, - "smartTransactionsBenefit3": { - "message": "Обновления в реальном времени" - }, - "smartTransactionsDescription": { - "message": "Откройте для себя более высокие коэффициенты успеха, передовую защиту и лучшую прозрачность с помощью умных транзакций." - }, - "smartTransactionsDescription2": { - "message": "Доступно только на Ethereum. Включайте или отключайте в любое время в настройках. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Улучшенная защита транзакций" - }, "snapAccountCreated": { "message": "Счет создан" }, @@ -4858,9 +4779,6 @@ "message": "Свяжитесь с авторами $1 для получения дополнительной поддержки.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Некоторые сети могут представлять угрозу безопасности и/или конфиденциальности. Прежде чем добавлять и использовать сеть, ознакомьтесь с рисками." - }, "somethingDoesntLookRight": { "message": "Что-то не так? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Выполнить стейкинг" }, - "startYourJourney": { - "message": "Начните свое путешествие с $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Начните использовать Web3, добавив $1 в свой кошелек.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Ошибка при получении журналов состояния." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Смотреть активность" }, - "viewAllDetails": { - "message": "Смотреть все сведения" - }, "viewAllQuotes": { "message": "смотреть все котировки" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 61d8ff6e5d8c..41612a6ce177 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Magdagdag ng custom na network" }, - "addEthereumChainConfirmationDescription": { - "message": "Magpapahintulot ito sa network na ito na gamitin sa loob ng MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "Ang MetaMask ay hindi nagve-verify ng mga custom na network." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Alamin ang tungkol sa $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "mga panloloko at panganib ng seguridad ng network", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Payagan ang site na ito na magdagdag ng network?" - }, "addEthereumChainWarningModalHeader": { "message": "Idagdag lamang ang RPC provider na ito kung sigurado kang mapagkakatiwalaan ito. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Bilhin Ngayon" }, - "buyToken": { - "message": "Bumili ng $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "I-edit ang pagpapabilis ng bayad sa gas" }, - "enable": { - "message": "Payagan" - }, "enableAutoDetect": { "message": " Paganahin ang autodetect" }, @@ -1720,10 +1696,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "May error habang kinukuha ang ligtas na chain list, mangyaring magpatuloy nang may pag-iingat." }, @@ -1735,14 +1707,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukang muling i-reload ang page, o kontakin ang support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan muli sa pamamagitan ng pagsasara o muling pagbubukas ng pop-up, kontakin ang support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Uri ng Function" }, - "fundYourWallet": { - "message": "Pondohan ang iyong wallet" - }, - "fundYourWalletDescription": { - "message": "Magsimula sa pamamagitan ng pagdagdag ng $1 sa iyong wallet.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gas" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Tingnan ang account sa $1" }, - "getStartedWithNFTs": { - "message": "Kumuha ng $1 para bumili ng mga NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Magsimula sa mga NFT sa pamamagitan ng pagdagdag ng $1 sa iyong wallet.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Bumalik" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Nagsa-sign in gamit ang" }, - "simulationDetailsFailed": { - "message": "Mayroong error sa pag-load ng iyong pagtataya." - }, "simulationDetailsFiatNotAvailable": { "message": "Hindi Available" }, "simulationDetailsIncomingHeading": { "message": "Natanggap mo" }, - "simulationDetailsNoBalanceChanges": { - "message": "Walang pagbabagong nahulaan para sa iyong wallet" - }, "simulationDetailsOutgoingHeading": { "message": "Nagpadala ka" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Mga Mapagkukunan" }, - "siweSignatureSimulationDetailInfo": { - "message": "Ikaw ay nagsa-sign in sa isang site at walang mga inaasahang pagbabago sa iyong account." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Mga Smart Transaction" }, - "smartTransactionsBenefit1": { - "message": "99.5% tiyansa ng tagumpay" - }, - "smartTransactionsBenefit2": { - "message": "Makatitipid ng pera" - }, - "smartTransactionsBenefit3": { - "message": "Mga real-time na update" - }, - "smartTransactionsDescription": { - "message": "Mag-unlock na mas mataas na tiyansa ng tagumpay, proteksyon sa frontrunning, at mas mahusay na visibility sa mga Smart Transaction." - }, - "smartTransactionsDescription2": { - "message": "Available lamang sa Ethereum. I-enable o i-disable anumang oras sa mga setting. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Pinahusay na Proteksyon sa Transaksyon" - }, "snapAccountCreated": { "message": "Nagawa ang account" }, @@ -4858,9 +4779,6 @@ "message": "Makipag-ugnayan sa mga tagalikha ng $1 para sa karagdagang suporta.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Maaaring magdulot ang ilang network ng mga panganib sa seguridad at/o pagkapribado. Unawain ang mga panganib bago idagdag o gamitin ang isang network." - }, "somethingDoesntLookRight": { "message": "Mayroon bang hindi tama? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Mag-stake" }, - "startYourJourney": { - "message": "Simulan ang iyong paglalakbay sa $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Magsimula sa web3 sa pamamagitan ng pagdagdag ng $1 sa iyong wallet.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Error sa pagkuha ng mga log ng estado." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Tingnan ang aktibidad" }, - "viewAllDetails": { - "message": "Tingnan ang lahat ng detalye" - }, "viewAllQuotes": { "message": "tingnan ang lahat ng quote" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index d80d6564b880..f11cc0d17523 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Özel ağ ekle" }, - "addEthereumChainConfirmationDescription": { - "message": "Bu, bu ağın MetaMas dahilinde kullanılmasına olanak tanıyacaktır." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask özel ağları doğrulamaz." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "$1 hakkında bilgi edinin.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "dolandırıcılık ve ağ güvenliği riskleri", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Bu sitenin ağ eklemesine izin ver?" - }, "addEthereumChainWarningModalHeader": { "message": "Bu RPC sağlayıcısını sadece ona güvenebileceğinizden eminseniz ekleyin. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Şimdi Satın Al" }, - "buyToken": { - "message": "$1 Al", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bayt" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Hızlandırma gaz ücretini düzenle" }, - "enable": { - "message": "Etkinleştir" - }, "enableAutoDetect": { "message": " Otomatik algılamayı etkinleştir" }, @@ -1720,10 +1696,6 @@ "message": "Kod: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Hata ayrıntıları", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Güvenli zincir listesi alınırken hata oluştu, lütfen dikkatli bir şekilde devam edin." }, @@ -1735,14 +1707,6 @@ "message": "Kod: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Sayfayı yeniden yükleyerek tekrar deneyin veya $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Açılır pencereyi kapatarak ve yeniden açarak tekrar deneyin $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask bir hata ile karşılaştı", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "İşlev türü" }, - "fundYourWallet": { - "message": "Cüzdanınıza para ekleyin" - }, - "fundYourWalletDescription": { - "message": "Cüzdanınıza biraz $1 ekleyerek başlayın.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gaz" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Hesabı $1 üzerinde görüntüleyin" }, - "getStartedWithNFTs": { - "message": "NFT satın almak için $1 edinin", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Cüzdanınıza biraz $1 ekleyerek NFT'lere başlayın.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Geri git" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Şununla giriş yap:" }, - "simulationDetailsFailed": { - "message": "Tahmininiz yüklenirken bir hata oldu." - }, "simulationDetailsFiatNotAvailable": { "message": "Mevcut Değil" }, "simulationDetailsIncomingHeading": { "message": "Aldığınız" }, - "simulationDetailsNoBalanceChanges": { - "message": "Cüzdanınız için değişiklik öngörülmüyor" - }, "simulationDetailsOutgoingHeading": { "message": "Gönderdiğiniz" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Kaynaklar" }, - "siweSignatureSimulationDetailInfo": { - "message": "Bir siteye giriş yapıyorsunuz ve hesabınızda öngörülen herhangi bir değişiklik yok." - }, "siweURI": { "message": "URL adresi" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Akıllı İşlemler" }, - "smartTransactionsBenefit1": { - "message": "%99,5 başarı oranı" - }, - "smartTransactionsBenefit2": { - "message": "Paradan tasarruf sağlar" - }, - "smartTransactionsBenefit3": { - "message": "Gerçek zamanlı güncellemeler" - }, - "smartTransactionsDescription": { - "message": "Akıllı İşlemler ile daha yüksek başarı oranlarının, arkadan çalıştırma korumasının ve daha iyi görünürlüğün kilidini açın." - }, - "smartTransactionsDescription2": { - "message": "Sadece Ethereum'da mevcuttur. Dilediğiniz zaman ayarlar kısmında etkinleştirin veya devre dışı bırakın. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "İyileştirilmiş İşlem Koruması" - }, "snapAccountCreated": { "message": "Hesap oluşturuldu" }, @@ -4858,9 +4779,6 @@ "message": "Daha fazla destek için $1 oluşturucuları ile iletişime geçin.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Bazı ağlar güvenlik ve/veya gizlilik riskleri teşkil edebilir. Bir ağ eklemeden ve kullanmadan önce riskleri anlayın." - }, "somethingDoesntLookRight": { "message": "Doğru görünmeyen bir şeyler mi var? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Pay" }, - "startYourJourney": { - "message": "$1 ile yolculuğunuza başlayın", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Cüzdanınıza biraz $1 ekleyerek web3'e başlayın.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Durum günlükleri alınırken hata." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Aktiviteyi görüntüle" }, - "viewAllDetails": { - "message": "Tüm bilgileri görüntüle" - }, "viewAllQuotes": { "message": "tüm teklifleri görüntüle" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 0bac5423d1ee..dd518df1bf22 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "Thêm mạng tùy chỉnh" }, - "addEthereumChainConfirmationDescription": { - "message": "Thao tác này sẽ cho phép sử dụng mạng này trong MetaMask." - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask không xác minh mạng tùy chỉnh." - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "Tìm hiểu về $1.", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "lừa đảo và các nguy cơ về an ninh mạng", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "Cho phép trang này thêm một mạng?" - }, "addEthereumChainWarningModalHeader": { "message": "Chỉ thêm nhà cung cấp RPC này nếu bạn chắc chắn bạn có thể tin tưởng. $1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "Mua ngay" }, - "buyToken": { - "message": "Mua $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Byte" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Chỉnh sửa phí gas tăng tốc" }, - "enable": { - "message": "Bật" - }, "enableAutoDetect": { "message": " Bật tự động phát hiện" }, @@ -1720,10 +1696,6 @@ "message": "Mã: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Chi tiết về lỗi", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Lỗi khi lấy danh sách chuỗi an toàn, vui lòng tiếp tục một cách thận trọng." }, @@ -1735,14 +1707,6 @@ "message": "Mã: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Hãy thử lại bằng cách tải lại trang hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Hãy thử lại bằng cách đóng và mở lại cửa sổ bật lên hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask đã gặp lỗi", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "Loại chức năng" }, - "fundYourWallet": { - "message": "Nạp tiền vào ví của bạn" - }, - "fundYourWalletDescription": { - "message": "Hãy bắt đầu bằng cách nạp một ít $1 vào ví của bạn.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Gas" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "Xem tài khoản trên $1" }, - "getStartedWithNFTs": { - "message": "Nhận $1 để mua NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "Hãy bắt đầu với NFT bằng cách nạp một ít $1 vào ví của bạn.", - "description": "$1 is the token symbol" - }, "goBack": { "message": "Quay Lại" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "Đăng nhập bằng" }, - "simulationDetailsFailed": { - "message": "Đã xảy ra lỗi khi tải kết quả ước tính." - }, "simulationDetailsFiatNotAvailable": { "message": "Không có sẵn" }, "simulationDetailsIncomingHeading": { "message": "Bạn nhận được" }, - "simulationDetailsNoBalanceChanges": { - "message": "Ví của bạn dự kiến không có thay đổi nào" - }, "simulationDetailsOutgoingHeading": { "message": "Bạn gửi" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "Tài nguyên" }, - "siweSignatureSimulationDetailInfo": { - "message": "Bạn đang đăng nhập vào một trang web và dự kiến không có thay đổi nào đối với tài khoản của bạn." - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "Giao dịch thông minh" }, - "smartTransactionsBenefit1": { - "message": "Tỷ lệ thành công 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Tiết kiệm tiền của bạn" - }, - "smartTransactionsBenefit3": { - "message": "Cập nhật theo thời gian thực" - }, - "smartTransactionsDescription": { - "message": "Đạt tỷ lệ thành công cao hơn, bảo vệ chống hành vi lợi dụng thông tin biết trước và khả năng hiển thị tốt hơn với Giao dịch thông minh." - }, - "smartTransactionsDescription2": { - "message": "Chỉ có sẵn trên Ethereum. Có thể bật/tắt bất cứ lúc nào trong phần Cài đặt. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Tăng cường bảo vệ giao dịch" - }, "snapAccountCreated": { "message": "Tài khoản đã được tạo" }, @@ -4858,9 +4779,6 @@ "message": "Liên hệ với những người tạo ra $1 để được hỗ trợ thêm.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "Một số mạng có thể gây ra rủi ro về bảo mật và/hoặc quyền riêng tư. Bạn cần hiểu rõ các rủi ro này trước khi thêm và sử dụng mạng." - }, "somethingDoesntLookRight": { "message": "Có gì đó không ổn? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "Stake" }, - "startYourJourney": { - "message": "Bắt đầu hành trình của bạn với $1", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "Hãy bắt đầu với Web3 bằng cách nạp một ít $1 vào ví của bạn.", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "Lỗi khi truy xuất nhật ký trạng thái." }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "Xem hoạt động" }, - "viewAllDetails": { - "message": "Xem toàn bộ chi tiết" - }, "viewAllQuotes": { "message": "xem tất cả báo giá" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 7209fb1c5b44..818f1ffdb82b 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -207,23 +207,6 @@ "addCustomNetwork": { "message": "添加自定义网络" }, - "addEthereumChainConfirmationDescription": { - "message": "这将允许在 MetaMask 中使用此网络。" - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask 不验证自定义网络。" - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "了解 $1。", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "欺诈和网络安全风险", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "允许此网站添加一个网络到MetaMask上?" - }, "addEthereumChainWarningModalHeader": { "message": "仅当您确定可以信任此 RPC 提供商时才能添加它。$1", "description": "$1 is addEthereumChainWarningModalHeaderPartTwo passed separately so that it can be bolded" @@ -822,10 +805,6 @@ "buyNow": { "message": "立即购买" }, - "buyToken": { - "message": "购买 $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "字节" }, @@ -1641,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "编辑加速燃料费用" }, - "enable": { - "message": "启用" - }, "enableAutoDetect": { "message": " 启用自动检测" }, @@ -1720,10 +1696,6 @@ "message": "代码:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "错误详情", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "获取安全链列表时出错,请谨慎继续。" }, @@ -1735,14 +1707,6 @@ "message": "代码:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "通过重新加载页面再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "通过关闭并重新打开弹出窗口再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遇到了一个错误", "description": "Title of generic error page" @@ -1900,13 +1864,6 @@ "functionType": { "message": "功能类型" }, - "fundYourWallet": { - "message": "向您的钱包存入资金" - }, - "fundYourWalletDescription": { - "message": "将一些 $1 添加到您的钱包并开始使用", - "description": "$1 is the token symbol" - }, "gas": { "message": "燃料" }, @@ -1990,14 +1947,6 @@ "genericExplorerView": { "message": "在$1查看账户" }, - "getStartedWithNFTs": { - "message": "获取 $1 以购买 NFT", - "description": "$1 is the token symbol" - }, - "getStartedWithNFTsDescription": { - "message": "将一些 $1 添加到您的钱包并开始使用 NFT", - "description": "$1 is the token symbol" - }, "goBack": { "message": "返回" }, @@ -4579,18 +4528,12 @@ "signingInWith": { "message": "使用以下登录方式" }, - "simulationDetailsFailed": { - "message": "加载估算时出错。" - }, "simulationDetailsFiatNotAvailable": { "message": "不可用" }, "simulationDetailsIncomingHeading": { "message": "您收到" }, - "simulationDetailsNoBalanceChanges": { - "message": "预计您的钱包不会发生变化" - }, "simulationDetailsOutgoingHeading": { "message": "您发送" }, @@ -4628,9 +4571,6 @@ "siweResources": { "message": "资源" }, - "siweSignatureSimulationDetailInfo": { - "message": "您正在登录某个网站,并且您的账户没有预期变化。" - }, "siweURI": { "message": "URL" }, @@ -4673,25 +4613,6 @@ "smartTransactions": { "message": "智能交易" }, - "smartTransactionsBenefit1": { - "message": "99.5%的成功率" - }, - "smartTransactionsBenefit2": { - "message": "为您省钱" - }, - "smartTransactionsBenefit3": { - "message": "实时更新" - }, - "smartTransactionsDescription": { - "message": "通过智能交易解锁更高的成功率、抢先交易保护和更高的透明度。" - }, - "smartTransactionsDescription2": { - "message": "仅适用于以太坊。可随时在设置中启用或禁用。$1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "增强型交易保护" - }, "snapAccountCreated": { "message": "账户已创建" }, @@ -4858,9 +4779,6 @@ "message": "联系 $1 的创建者以获得进一步支持。", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, - "someNetworksMayPoseSecurity": { - "message": "某些网络可能会带来安全和/或隐私风险。在添加和使用网络之前,请先了解风险。" - }, "somethingDoesntLookRight": { "message": "有什么不对劲吗?$1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5014,14 +4932,6 @@ "stake": { "message": "质押" }, - "startYourJourney": { - "message": "从 $1 开始您的旅程", - "description": "$1 is the token symbol" - }, - "startYourJourneyDescription": { - "message": "将一些 $1 添加到您的钱包并开始使用 Web3", - "description": "$1 is the token symbol" - }, "stateLogError": { "message": "检索状态日志时出错。" }, @@ -6007,9 +5917,6 @@ "viewActivity": { "message": "查看活动" }, - "viewAllDetails": { - "message": "查看所有详情" - }, "viewAllQuotes": { "message": "查看所有报价" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 7cdfa8e28add..29953f67c941 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -42,23 +42,6 @@ "addContact": { "message": "新增合約" }, - "addEthereumChainConfirmationDescription": { - "message": "這會允許在 MetaMask 內使用這個網路。" - }, - "addEthereumChainConfirmationRisks": { - "message": "MetaMask 不會對自訂的網路做驗證。" - }, - "addEthereumChainConfirmationRisksLearnMore": { - "message": "了解更多關於$1的事。", - "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" - }, - "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "詐騙與網路安全風險", - "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" - }, - "addEthereumChainConfirmationTitle": { - "message": "允許這個網站新增一個網路?" - }, "addFriendsAndAddresses": { "message": "新增朋友和您信任的位址" }, @@ -442,10 +425,6 @@ "message": "代碼:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "錯誤詳細資訊", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "訊息:$1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -454,14 +433,6 @@ "message": "代碼:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "重新整理頁面然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "重新開啟彈跳視窗然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遭遇錯誤", "description": "Title of generic error page" @@ -1370,9 +1341,6 @@ "userName": { "message": "使用者名稱" }, - "viewAllDetails": { - "message": "查看所有詳情" - }, "viewContact": { "message": "觀看聯絡資訊" }, diff --git a/app/images/ape-token.svg b/app/images/ape-token.svg new file mode 100644 index 000000000000..8c1777f1a497 --- /dev/null +++ b/app/images/ape-token.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/gravity.svg b/app/images/gravity.svg new file mode 100644 index 000000000000..a94d44f8d285 --- /dev/null +++ b/app/images/gravity.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/images/icons/collapse.svg b/app/images/icons/collapse.svg new file mode 100644 index 000000000000..5ec98b457f78 --- /dev/null +++ b/app/images/icons/collapse.svg @@ -0,0 +1 @@ + diff --git a/app/images/ramps-card-nft-illustration.png b/app/images/ramps-card-nft-illustration.png deleted file mode 100644 index 1cbc824592f8..000000000000 Binary files a/app/images/ramps-card-nft-illustration.png and /dev/null differ diff --git a/app/images/solana-logo.svg b/app/images/solana-logo.svg new file mode 100644 index 000000000000..ed6f34d95f7e --- /dev/null +++ b/app/images/solana-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/scripts/background.js b/app/scripts/background.js index ad6e3b6f22c2..90a52b6c0d19 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,6 +56,8 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; +import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; +import { checkURLForProviderInjection } from '../../shared/modules/provider-injection'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -298,6 +300,9 @@ function maybeDetectPhishing(theController) { category: MetaMetricsEventCategory.Phishing, properties: { url: hostname, + referrer: { + url: hostname, + }, reason: blockReason, }, }); @@ -330,6 +335,40 @@ function maybeDetectPhishing(theController) { ); } +/** + * Overrides the Content-Security-Policy (CSP) header by adding a nonce to the `script-src` directive. + * This is a workaround for [Bug #1446231](https://bugzilla.mozilla.org/show_bug.cgi?id=1446231), + * which involves overriding the page CSP for inline script nodes injected by extension content scripts. + */ +function overrideContentSecurityPolicyHeader() { + // The extension url is unique per install on Firefox, so we can safely add it as a nonce to the CSP header + const nonce = btoa(browser.runtime.getURL('/')); + browser.webRequest.onHeadersReceived.addListener( + ({ responseHeaders, url }) => { + // Check whether inpage.js is going to be injected into the page or not. + // There is no reason to modify the headers if we are not injecting inpage.js. + const isInjected = checkURLForProviderInjection(new URL(url)); + + // Check if the user has enabled the overrideContentSecurityPolicyHeader preference + const isEnabled = + controller.preferencesController.state + .overrideContentSecurityPolicyHeader; + + if (isInjected && isEnabled) { + for (const header of responseHeaders) { + if (header.name.toLowerCase() === 'content-security-policy') { + header.value = addNonceToCsp(header.value, nonce); + } + } + } + + return { responseHeaders }; + }, + { types: ['main_frame', 'sub_frame'], urls: ['http://*/*', 'https://*/*'] }, + ['blocking', 'responseHeaders'], + ); +} + // These are set after initialization let connectRemote; let connectExternalExtension; @@ -476,6 +515,11 @@ async function initialize() { if (!isManifestV3) { await loadPhishingWarningPage(); + // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts + // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 + if (getPlatform() === PLATFORM_FIREFOX) { + overrideContentSecurityPolicyHeader(); + } } await sendReadyMessageToTabs(); log.info('MetaMask initialization complete.'); diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 76fb2386f1f6..655851590441 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -98,6 +98,7 @@ export const SENTRY_BACKGROUND_STATE = { BridgeController: { bridgeState: { bridgeFeatureFlags: { + extensionConfig: false, extensionSupport: false, destNetworkAllowlist: [], srcNetworkAllowlist: [], @@ -106,6 +107,18 @@ export const SENTRY_BACKGROUND_STATE = { destTopAssets: [], srcTokens: {}, srcTopAssets: [], + quoteRequest: { + walletAddress: false, + srcTokenAddress: true, + slippage: true, + srcChainId: true, + destChainId: true, + destTokenAddress: true, + srcTokenAmount: true, + }, + quotes: [], + quotesLastFetched: true, + quotesLoadingStatus: true, }, }, CronjobController: { @@ -210,6 +223,7 @@ export const SENTRY_BACKGROUND_STATE = { advancedGasFee: true, currentLocale: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: true, forgottenPassword: true, identities: false, @@ -233,6 +247,7 @@ export const SENTRY_BACKGROUND_STATE = { showNativeTokenAsMainBalance: true, petnamesEnabled: true, showConfirmationAdvancedDetails: true, + privacyMode: false, }, useExternalServices: false, selectedAddress: false, diff --git a/app/scripts/controllers/alert-controller.test.ts b/app/scripts/controllers/alert-controller.test.ts index a8aee606e02d..de314c31f050 100644 --- a/app/scripts/controllers/alert-controller.test.ts +++ b/app/scripts/controllers/alert-controller.test.ts @@ -2,16 +2,16 @@ * @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, + AlertControllerMessenger, + AlertControllerGetStateAction, + AlertControllerStateChangeEvent, + AlertControllerOptions, + getDefaultAlertControllerState, } from './alert-controller'; const EMPTY_ACCOUNT = { @@ -28,230 +28,153 @@ const EMPTY_ACCOUNT = { importTime: 0, }, }; -describe('AlertController', () => { - let controllerMessenger: ControllerMessenger< - AlertControllerActions | AllowedActions, - | AlertControllerEvents - | KeyringControllerStateChangeEvent - | SnapControllerStateChangeEvent - | AllowedEvents + +type WithControllerOptions = Partial; + +type WithControllerCallback = ({ + controller, +}: { + controller: AlertController; + messenger: ControllerMessenger< + AllowedActions | AlertControllerGetStateAction, + AllowedEvents | AlertControllerStateChangeEvent >; - let alertController: AlertController; +}) => ReturnValue; + +type WithControllerArgs = + | [WithControllerCallback] + | [WithControllerOptions, WithControllerCallback]; - beforeEach(() => { - controllerMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents - >(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => EMPTY_ACCOUNT, - ); +async function withController( + ...args: WithControllerArgs +): Promise { + const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; + const { ...alertControllerOptions } = rest; - const alertMessenger = controllerMessenger.getRestricted({ + const controllerMessenger = new ControllerMessenger< + AllowedActions | AlertControllerGetStateAction, + AllowedEvents | AlertControllerStateChangeEvent + >(); + + const alertControllerMessenger: AlertControllerMessenger = + controllerMessenger.getRestricted({ name: 'AlertController', - allowedActions: [`AccountsController:getSelectedAccount`], - allowedEvents: [`AccountsController:selectedAccountChange`], + allowedActions: ['AccountsController:getSelectedAccount'], + allowedEvents: ['AccountsController:selectedAccountChange'], }); - alertController = new AlertController({ - state: { - unconnectedAccountAlertShownOrigins: { - testUnconnectedOrigin: false, - }, - web3ShimUsageOrigins: { - testWeb3ShimUsageOrigin: 0, - }, - }, - controllerMessenger: alertMessenger, - }); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + jest.fn().mockReturnValue(EMPTY_ACCOUNT), + ); + + const controller = new AlertController({ + messenger: alertControllerMessenger, + ...alertControllerOptions, }); + return await fn({ + controller, + messenger: controllerMessenger, + }); +} + +describe('AlertController', () => { 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, - }, + it('should be same as AlertControllerState initialized', async () => { + await withController(({ controller }) => { + expect(controller.state).toStrictEqual( + getDefaultAlertControllerState(), + ); }); }); }); describe('alertEnabledness', () => { - it('should default unconnectedAccount of alertEnabledness to true', () => { - expect( - alertController.store.getState().alertEnabledness.unconnectedAccount, - ).toStrictEqual(true); + it('should default unconnectedAccount of alertEnabledness to true', async () => { + await withController(({ controller }) => { + expect( + controller.state.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); + it('should set unconnectedAccount of alertEnabledness to false', async () => { + await withController(({ controller }) => { + controller.setAlertEnabledness('unconnectedAccount', false); + expect( + controller.state.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 default unconnectedAccountAlertShownOrigins', async () => { + await withController(({ controller }) => { + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); }); }); - it('should set unconnectedAccountAlertShownOrigins', () => { - alertController.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: true, - }); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: true, + it('should set unconnectedAccountAlertShownOrigins', async () => { + await withController(({ controller }) => { + controller.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); + expect( + controller.state.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 default web3ShimUsageOrigins', async () => { + await withController(({ controller }) => { + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({}); }); }); - 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 recorded', async () => { + await withController(({ controller }) => { + controller.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + expect(controller.state.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, + it('should set origin of web3ShimUsageOrigins to dismissed', async () => { + await withController(({ controller }) => { + controller.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); + expect(controller.state.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: '', + it('should set unconnectedAccountAlertShownOrigins to {}', async () => { + await withController(({ controller, messenger }) => { + messenger.publish('AccountsController:selectedAccountChange', { + id: '', + address: '0x1234567', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, }, - 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, + }); + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); }); }); }); diff --git a/app/scripts/controllers/alert-controller.ts b/app/scripts/controllers/alert-controller.ts index 9e1882035e02..90e177e9edca 100644 --- a/app/scripts/controllers/alert-controller.ts +++ b/app/scripts/controllers/alert-controller.ts @@ -1,9 +1,13 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { TOGGLEABLE_ALERT_TYPES, Web3ShimUsageAlertStates, @@ -14,10 +18,10 @@ const controllerName = 'AlertController'; /** * Returns the state of the {@link AlertController}. */ -export type AlertControllerGetStateAction = { - type: 'AlertController:getState'; - handler: () => AlertControllerState; -}; +export type AlertControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AlertControllerState +>; /** * Actions exposed by the {@link AlertController}. @@ -27,10 +31,10 @@ export type AlertControllerActions = AlertControllerGetStateAction; /** * Event emitted when the state of the {@link AlertController} changes. */ -export type AlertControllerStateChangeEvent = { - type: 'AlertController:stateChange'; - payload: [AlertControllerState, []]; -}; +export type AlertControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + AlertControllerState +>; /** * Events emitted by {@link AlertController}. @@ -76,12 +80,15 @@ export type AlertControllerState = { * @property state - The initial controller state * @property controllerMessenger - The controller messenger */ -type AlertControllerOptions = { +export type AlertControllerOptions = { state?: Partial; - controllerMessenger: AlertControllerMessenger; + messenger: AlertControllerMessenger; }; -const defaultState: AlertControllerState = { +/** + * Function to get default state of the {@link AlertController}. + */ +export const getDefaultAlertControllerState = (): AlertControllerState => ({ alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( (alertEnabledness: Record, alertType: string) => { alertEnabledness[alertType] = true; @@ -91,61 +98,76 @@ const defaultState: AlertControllerState = { ), unconnectedAccountAlertShownOrigins: {}, web3ShimUsageOrigins: {}, +}); + +/** + * {@link AlertController}'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 = { + alertEnabledness: { + persist: true, + anonymous: true, + }, + unconnectedAccountAlertShownOrigins: { + persist: true, + anonymous: false, + }, + web3ShimUsageOrigins: { + persist: true, + anonymous: false, + }, }; /** * Controller responsible for maintaining alert-related state. */ -export class AlertController { - store: ObservableStore; - - readonly #controllerMessenger: AlertControllerMessenger; - +export class AlertController extends BaseController< + typeof controllerName, + AlertControllerState, + 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, - [], - ); + super({ + messenger: opts.messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultAlertControllerState(), + ...opts.state, + }, }); - this.#selectedAddress = this.#controllerMessenger.call( + this.#selectedAddress = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ).address; - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', (account: { address: string }) => { - const currentState = this.store.getState(); + const currentState = this.state; if ( currentState.unconnectedAccountAlertShownOrigins && this.#selectedAddress !== account.address ) { this.#selectedAddress = account.address; - this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); + this.update((state) => { + state.unconnectedAccountAlertShownOrigins = {}; + }); } }, ); } setAlertEnabledness(alertId: string, enabledness: boolean): void { - const { alertEnabledness } = this.store.getState(); - alertEnabledness[alertId] = enabledness; - this.store.updateState({ alertEnabledness }); + this.update((state) => { + state.alertEnabledness[alertId] = enabledness; + }); } /** @@ -154,9 +176,9 @@ export class AlertController { * @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 }); + this.update((state) => { + state.unconnectedAccountAlertShownOrigins[origin] = true; + }); } /** @@ -167,7 +189,7 @@ export class AlertController { * origin, or undefined. */ getWeb3ShimUsageState(origin: string): number | undefined { - return this.store.getState().web3ShimUsageOrigins?.[origin]; + return this.state.web3ShimUsageOrigins?.[origin]; } /** @@ -194,10 +216,10 @@ export class AlertController { * @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 }); - } + this.update((state) => { + if (state.web3ShimUsageOrigins) { + state.web3ShimUsageOrigins[origin] = value; + } + }); } } diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 25b6eae98c33..35449cb40764 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -2,6 +2,10 @@ 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 { flushPromises } from '../../../../test/lib/timer-helpers'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import * as bridgeUtil from '../../../../ui/pages/bridge/bridge.util'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; @@ -26,9 +30,15 @@ describe('BridgeController', function () { beforeEach(() => { jest.clearAllMocks(); + jest.clearAllTimers(); + nock(BRIDGE_API_BASE_URL) .get('/getAllFeatureFlags') .reply(200, { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + }, 'extension-support': true, 'src-network-allowlist': [10, 534352], 'dest-network-allowlist': [137, 42161], @@ -55,6 +65,7 @@ describe('BridgeController', function () { symbol: 'ABC', }, ]); + bridgeController.resetState(); }); it('constructor should setup correctly', function () { @@ -66,13 +77,35 @@ describe('BridgeController', function () { extensionSupport: true, destNetworkAllowlist: [CHAIN_IDS.POLYGON, CHAIN_IDS.ARBITRUM], srcNetworkAllowlist: [CHAIN_IDS.OPTIMISM, CHAIN_IDS.SCROLL], + extensionConfig: { + maxRefreshCount: 1, + refreshRate: 3, + }, }; expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + const setIntervalLengthSpy = jest.spyOn( + bridgeController, + 'setIntervalLength', + ); + await bridgeController.setBridgeFeatureFlags(); expect(bridgeController.state.bridgeState.bridgeFeatureFlags).toStrictEqual( expectedFeatureFlagsResponse, ); + expect(setIntervalLengthSpy).toHaveBeenCalledTimes(1); + expect(setIntervalLengthSpy).toHaveBeenCalledWith(3); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + bridgeFeatureFlags: expectedFeatureFlagsResponse, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); }); it('selectDestNetwork should set the bridge dest tokens and top assets', async function () { @@ -94,6 +127,11 @@ describe('BridgeController', function () { expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, ]); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); }); it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { @@ -118,5 +156,240 @@ describe('BridgeController', function () { symbol: 'ABC', }, ]); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should update the quoteRequest state', function () { + bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: 10, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: undefined, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: undefined, + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: '0x2ABC', + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x2ABC', + walletAddress: undefined, + }); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should trigger quote polling if request is valid', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingByNetworkClientIdSpy = jest.spyOn( + bridgeController, + 'startPollingByNetworkClientId', + ); + messengerMock.call.mockReturnValue({ address: '0x123' } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(bridgeUtil, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([1, 2, 3] as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([5, 6, 7] as never); + }, 10000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((_, reject) => { + return setTimeout(() => { + reject(new Error('Network error')); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledWith( + '1', + quoteRequest, + ); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith(quoteRequest); + + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched ?? 0; + expect(firstFetchTime).toBeGreaterThan(0); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [1, 2, 3], + quotesLoadingStatus: 1, + }), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + firstFetchTime, + ); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [5, 6, 7], + quotesLoadingStatus: 1, + }), + ); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(secondFetchTime).toBeGreaterThan(firstFetchTime); + + // After 3nd fetch throws an error + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [5, 6, 7], + quotesLoadingStatus: 2, + }), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + secondFetchTime, + ); + }); + + it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () { + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingByNetworkClientIdSpy = jest.spyOn( + bridgeController, + 'startPollingByNetworkClientId', + ); + messengerMock.call.mockReturnValueOnce({ address: '0x123' } as never); + + bridgeController.updateBridgeQuoteRequestParams({ + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + }); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).not.toHaveBeenCalled(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + destChainId: 10, + destTokenAddress: '0x123', + }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 841d735ac52c..1d20e6f404e4 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -1,7 +1,10 @@ -import { BaseController, StateMetadata } from '@metamask/base-controller'; +import { StateMetadata } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { NetworkClientId } from '@metamask/network-controller'; import { fetchBridgeFeatureFlags, + fetchBridgeQuotes, fetchBridgeTokens, // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -9,11 +12,24 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; +import { decimalToHex } from '../../../../shared/modules/conversion.utils'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { QuoteRequest } from '../../../../ui/pages/bridge/types'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { isValidQuoteRequest } from '../../../../ui/pages/bridge/utils/quote'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, + REFRESH_INTERVAL_MS, + RequestStatus, } from './constants'; -import { BridgeControllerState, BridgeControllerMessenger } from './types'; +import { + BridgeControllerState, + BridgeControllerMessenger, + BridgeFeatureFlagsKey, +} from './types'; const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { bridgeState: { @@ -22,7 +38,7 @@ const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { }, }; -export default class BridgeController extends BaseController< +export default class BridgeController extends StaticIntervalPollingController< typeof BRIDGE_CONTROLLER_NAME, { bridgeState: BridgeControllerState }, BridgeControllerMessenger @@ -32,9 +48,13 @@ export default class BridgeController extends BaseController< name: BRIDGE_CONTROLLER_NAME, metadata, messenger, - state: { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE }, + state: { + bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, + }, }); + this.setIntervalLength(REFRESH_INTERVAL_MS); + this.messagingSystem.registerActionHandler( `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), @@ -47,12 +67,55 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, this.selectDestNetwork.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:updateBridgeQuoteRequestParams`, + this.updateBridgeQuoteRequestParams.bind(this), + ); } + _executePoll = async ( + _: NetworkClientId, + updatedQuoteRequest: QuoteRequest, + ) => { + await this.#fetchBridgeQuotes(updatedQuoteRequest); + }; + + updateBridgeQuoteRequestParams = (paramsToUpdate: Partial) => { + this.stopAllPolling(); + const { bridgeState } = this.state; + const updatedQuoteRequest = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, + ...paramsToUpdate, + }; + + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quoteRequest: updatedQuoteRequest, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }; + }); + + if (isValidQuoteRequest(updatedQuoteRequest)) { + const walletAddress = this.#getSelectedAccount().address; + this.startPollingByNetworkClientId( + decimalToHex(updatedQuoteRequest.srcChainId), + { ...updatedQuoteRequest, walletAddress }, + ); + } + }; + resetState = () => { + this.stopAllPolling(); this.update((_state) => { _state.bridgeState = { + ..._state.bridgeState, ...DEFAULT_BRIDGE_CONTROLLER_STATE, + quotes: [], + bridgeFeatureFlags: _state.bridgeState.bridgeFeatureFlags, }; }); }; @@ -63,6 +126,9 @@ export default class BridgeController extends BaseController< this.update((_state) => { _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; }); + this.setIntervalLength( + bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].refreshRate, + ); }; selectSrcNetwork = async (chainId: Hex) => { @@ -75,6 +141,36 @@ export default class BridgeController extends BaseController< await this.#setTokens(chainId, 'destTokens'); }; + #fetchBridgeQuotes = async (request: QuoteRequest) => { + const { bridgeState } = this.state; + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.LOADING, + }; + }); + + try { + const quotes = await fetchBridgeQuotes(request); + this.update((_state) => { + _state.bridgeState = { + ..._state.bridgeState, + quotes, + quotesLoadingStatus: RequestStatus.FETCHED, + }; + }); + } catch (error) { + console.log('Failed to fetch bridge quotes', error); + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quotesLoadingStatus: RequestStatus.ERROR, + }; + }); + } + }; + #setTopAssets = async ( chainId: Hex, stateKey: 'srcTopAssets' | 'destTopAssets', @@ -93,4 +189,8 @@ export default class BridgeController extends BaseController< _state.bridgeState = { ...bridgeState, [stateKey]: tokens }; }); }; + + #getSelectedAccount() { + return this.messagingSystem.call('AccountsController:getSelectedAccount'); + } } diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index 58c7d015b7bb..a4aa3264fdc8 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -1,9 +1,23 @@ +import { zeroAddress } from 'ethereumjs-util'; import { BridgeControllerState, BridgeFeatureFlagsKey } from './types'; export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; +export const REFRESH_INTERVAL_MS = 30 * 1000; +const DEFAULT_MAX_REFRESH_COUNT = 5; +const DEFAULT_SLIPPAGE = 0.5; + +export enum RequestStatus { + LOADING, + FETCHED, + ERROR, +} export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, + }, [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], @@ -12,4 +26,12 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { srcTopAssets: [], destTokens: {}, destTopAssets: [], + quoteRequest: { + walletAddress: undefined, + srcTokenAddress: zeroAddress(), + slippage: DEFAULT_SLIPPAGE, + }, + quotes: [], + quotesLastFetched: undefined, + quotesLoadingStatus: undefined, }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index 2fb36e1e983e..10c2d8646545 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -3,17 +3,26 @@ import { RestrictedControllerMessenger, } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import { SwapsTokenObject } from '../../../../shared/constants/swaps'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { QuoteRequest, QuoteResponse } from '../../../../ui/pages/bridge/types'; import BridgeController from './bridge-controller'; -import { BRIDGE_CONTROLLER_NAME } from './constants'; +import { BRIDGE_CONTROLLER_NAME, RequestStatus } from './constants'; export enum BridgeFeatureFlagsKey { + EXTENSION_CONFIG = 'extensionConfig', EXTENSION_SUPPORT = 'extensionSupport', NETWORK_SRC_ALLOWLIST = 'srcNetworkAllowlist', NETWORK_DEST_ALLOWLIST = 'destNetworkAllowlist', } export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + }; [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: boolean; [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: Hex[]; [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: Hex[]; @@ -25,11 +34,16 @@ export type BridgeControllerState = { srcTopAssets: { address: string }[]; destTokens: Record; destTopAssets: { address: string }[]; + quoteRequest: Partial; + quotes: QuoteResponse[]; + quotesLastFetched?: number; + quotesLoadingStatus?: RequestStatus; }; export enum BridgeUserAction { SELECT_SRC_NETWORK = 'selectSrcNetwork', SELECT_DEST_NETWORK = 'selectDestNetwork', + UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', } export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', @@ -44,20 +58,24 @@ type BridgeControllerAction = { type BridgeControllerActions = | BridgeControllerAction | BridgeControllerAction - | BridgeControllerAction; + | BridgeControllerAction + | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, BridgeControllerState >; +type AllowedActions = AccountsControllerGetSelectedAccountAction['type']; +type AllowedEvents = never; + /** * The messenger for the BridgeController. */ export type BridgeControllerMessenger = RestrictedControllerMessenger< typeof BRIDGE_CONTROLLER_NAME, - BridgeControllerActions, + BridgeControllerActions | AccountsControllerGetSelectedAccountAction, BridgeControllerEvents, - never, - never + AllowedActions, + AllowedEvents >; diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.ts similarity index 69% rename from app/scripts/controllers/metametrics.test.js rename to app/scripts/controllers/metametrics.test.ts index ca5602de33c8..4b2a1f09a562 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.ts @@ -1,18 +1,38 @@ import { toHex } from '@metamask/controller-utils'; -import { NameType } from '@metamask/name-controller'; +import { NetworkState } from '@metamask/network-controller'; +import { NameEntry, NameType } from '@metamask/name-controller'; +import { AddressBookEntry } from '@metamask/address-book-controller'; +import { + Nft, + Token, + TokensControllerState, +} from '@metamask/assets-controllers'; +import { InternalAccount } from '@metamask/keyring-api'; +import { Browser } from 'webextension-polyfill'; +import { Hex } from '@metamask/utils'; +import { merge } from 'lodash'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { createSegmentMock } from '../lib/segment'; import { METAMETRICS_ANONYMOUS_ID, METAMETRICS_BACKGROUND_PAGE_OBJECT, MetaMetricsUserTrait, + MetaMetricsUserTraits, } from '../../../shared/constants/metametrics'; import { CHAIN_IDS } from '../../../shared/constants/network'; +import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets'; import * as Utils from '../lib/util'; import { mockNetworkState } from '../../../test/stub/networks'; -import MetaMetricsController from './metametrics'; +import MetaMetricsController, { + MetaMetricsControllerOptions, + MetaMetricsControllerState, +} from './metametrics'; +import { + getDefaultPreferencesControllerState, + PreferencesControllerState, +} from './preferences-controller'; -const segment = createSegmentMock(2, 10000); +const segmentMock = createSegmentMock(2); const VERSION = '0.0.1-test'; const FAKE_CHAIN_ID = '0x1338'; @@ -27,7 +47,7 @@ const MOCK_EXTENSION = { id: MOCK_EXTENSION_ID, setUninstallURL: () => undefined, }, -}; +} as unknown as Browser; const MOCK_TRAITS = { test_boolean: true, @@ -36,12 +56,12 @@ const MOCK_TRAITS = { test_bool_array: [true, true, false], test_string_array: ['test', 'test', 'test'], test_boolean_array: [1, 2, 3], -}; +} as MetaMetricsUserTraits; const MOCK_INVALID_TRAITS = { test_null: null, test_array_multi_types: [true, 'a', 1], -}; +} as MetaMetricsUserTraits; const DEFAULT_TEST_CONTEXT = { app: { @@ -74,8 +94,19 @@ const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, }; -const SAMPLE_PERSISTED_EVENT = { - id: 'testid', +const SAMPLE_TX_SUBMITTED_PARTIAL_FRAGMENT = { + id: 'transaction-submitted-0000', + canDeleteIfAbandoned: true, + category: 'Unit Test', + successEvent: 'Transaction Finalized', + persist: true, + properties: { + simulation_response: 'no_balance_change', + test_stored_prop: 1, + }, +}; + +const SAMPLE_PERSISTED_EVENT_NO_ID = { persist: true, category: 'Unit Test', successEvent: 'sample persisted event success', @@ -85,6 +116,11 @@ const SAMPLE_PERSISTED_EVENT = { }, }; +const SAMPLE_PERSISTED_EVENT = { + id: 'testid', + ...SAMPLE_PERSISTED_EVENT_NO_ID, +}; + const SAMPLE_NON_PERSISTED_EVENT = { id: 'testid2', persist: false, @@ -101,7 +137,7 @@ function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, marketingCampaignCookieId = null, - preferencesControllerState = { currentLocale: LOCALE }, + currentLocale = LOCALE, onPreferencesStateChange = () => { // do nothing }, @@ -109,13 +145,26 @@ function getMetaMetricsController({ onNetworkDidChange = () => { // do nothing }, - segmentInstance, + segment = segmentMock, +}: { + currentLocale?: string; + participateInMetaMetrics?: MetaMetricsControllerState['participateInMetaMetrics']; + metaMetricsId?: MetaMetricsControllerState['metaMetricsId']; + dataCollectionForMarketing?: MetaMetricsControllerState['dataCollectionForMarketing']; + marketingCampaignCookieId?: MetaMetricsControllerState['marketingCampaignCookieId']; + onPreferencesStateChange?: MetaMetricsControllerOptions['onPreferencesStateChange']; + getCurrentChainId?: MetaMetricsControllerOptions['getCurrentChainId']; + onNetworkDidChange?: MetaMetricsControllerOptions['onNetworkDidChange']; + segment?: MetaMetricsControllerOptions['segment']; } = {}) { return new MetaMetricsController({ - segment: segmentInstance || segment, + segment, getCurrentChainId, onNetworkDidChange, - preferencesControllerState, + preferencesControllerState: { + ...getDefaultPreferencesControllerState(), + currentLocale, + }, onPreferencesStateChange, version: '0.0.1', environment: 'test', @@ -127,7 +176,6 @@ function getMetaMetricsController({ testid: SAMPLE_PERSISTED_EVENT, testid2: SAMPLE_NON_PERSISTED_EVENT, }, - events: {}, }, extension: MOCK_EXTENSION, }); @@ -143,7 +191,7 @@ describe('MetaMetricsController', function () { describe('constructor', function () { it('should properly initialize', function () { - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); const metaMetricsController = getMetaMetricsController(); expect(metaMetricsController.version).toStrictEqual(VERSION); expect(metaMetricsController.chainId).toStrictEqual(FAKE_CHAIN_ID); @@ -180,9 +228,13 @@ describe('MetaMetricsController', function () { }); it('should update when network changes', function () { - let chainId = '0x111'; - let networkDidChangeListener; - const onNetworkDidChange = (listener) => { + let chainId: Hex = '0x111'; + let networkDidChangeListener: (state: NetworkState) => void = () => { + // do nothing + }; + const onNetworkDidChange: ( + listener: (state: NetworkState) => void, + ) => void = (listener) => { networkDidChangeListener = listener; }; const metaMetricsController = getMetaMetricsController({ @@ -191,26 +243,214 @@ describe('MetaMetricsController', function () { }); chainId = '0x222'; - networkDidChangeListener(); + + networkDidChangeListener({} as NetworkState); expect(metaMetricsController.chainId).toStrictEqual('0x222'); }); it('should update when preferences changes', function () { - let subscribeListener; - const onPreferencesStateChange = (listener) => { - subscribeListener = listener; + let subscribeListener: ( + state: PreferencesControllerState, + ) => void = () => { + // do nothing }; + const onPreferencesStateChange: MetaMetricsControllerOptions['onPreferencesStateChange'] = + (listener) => { + subscribeListener = listener; + }; const metaMetricsController = getMetaMetricsController({ - preferencesControllerState: { currentLocale: LOCALE }, + currentLocale: LOCALE, onPreferencesStateChange, }); - subscribeListener({ currentLocale: 'en_UK' }); + subscribeListener({ + ...getDefaultPreferencesControllerState(), + currentLocale: 'en_UK', + }); expect(metaMetricsController.locale).toStrictEqual('en-UK'); }); }); + describe('createEventFragment', function () { + it('should throw an error if the param is missing successEvent or category', async function () { + const metaMetricsController = getMetaMetricsController(); + + await expect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error because we are testing the error case + metaMetricsController.createEventFragment({ event: 'test' }); + }).toThrow(/Must specify success event and category\./u); + + await expect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error because we are testing the error case + metaMetricsController.createEventFragment({ category: 'test' }); + }).toThrow(/Must specify success event and category\./u); + }); + + it('should update fragments state with new fragment', function () { + jest.useFakeTimers().setSystemTime(1730798301422); + + const metaMetricsController = getMetaMetricsController(); + const mockNewId = 'testid3'; + + metaMetricsController.createEventFragment({ + ...SAMPLE_PERSISTED_EVENT_NO_ID, + uniqueIdentifier: mockNewId, + }); + + const resultFragment = metaMetricsController.state.fragments[mockNewId]; + + expect(resultFragment).toStrictEqual({ + ...SAMPLE_PERSISTED_EVENT_NO_ID, + id: mockNewId, + uniqueIdentifier: mockNewId, + lastUpdated: 1730798301422, + }); + + jest.useRealTimers(); + }); + + it('should track the initial event if provided', function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + }); + const spy = jest.spyOn(segmentMock, 'track'); + const mockInitialEventName = 'Test Initial Event'; + + metaMetricsController.createEventFragment({ + ...SAMPLE_PERSISTED_EVENT_NO_ID, + initialEvent: mockInitialEventName, + }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should not call track if no initialEvent was provided', function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + }); + const spy = jest.spyOn(segmentMock, 'track'); + + metaMetricsController.createEventFragment({ + ...SAMPLE_PERSISTED_EVENT_NO_ID, + }); + + expect(spy).toHaveBeenCalledTimes(0); + }); + + describe('when intialEvent is "Transaction Submitted" and a fragment exists before createEventFragment is called', function () { + it('should update existing fragment state with new fragment props', function () { + jest.useFakeTimers().setSystemTime(1730798302222); + + const metaMetricsController = getMetaMetricsController(); + const { id } = SAMPLE_TX_SUBMITTED_PARTIAL_FRAGMENT; + + metaMetricsController.updateEventFragment( + SAMPLE_TX_SUBMITTED_PARTIAL_FRAGMENT.id, + { + ...SAMPLE_TX_SUBMITTED_PARTIAL_FRAGMENT, + }, + ); + metaMetricsController.createEventFragment({ + ...SAMPLE_PERSISTED_EVENT_NO_ID, + initialEvent: 'Transaction Submitted', + uniqueIdentifier: id, + }); + + const resultFragment = metaMetricsController.state.fragments[id]; + const expectedFragment = merge( + SAMPLE_TX_SUBMITTED_PARTIAL_FRAGMENT, + SAMPLE_PERSISTED_EVENT_NO_ID, + { + canDeleteIfAbandoned: false, + id, + initialEvent: 'Transaction Submitted', + uniqueIdentifier: id, + lastUpdated: 1730798302222, + }, + ); + + expect(resultFragment).toStrictEqual(expectedFragment); + + jest.useRealTimers(); + }); + }); + }); + + describe('updateEventFragment', function () { + beforeEach(function () { + jest.useFakeTimers().setSystemTime(1730798303333); + }); + afterEach(function () { + jest.useRealTimers(); + }); + + it('updates fragment with additional provided props', async function () { + const metaMetricsController = getMetaMetricsController(); + const MOCK_PROPS_TO_UPDATE = { + properties: { + test: 1, + }, + }; + + metaMetricsController.updateEventFragment( + SAMPLE_PERSISTED_EVENT.id, + MOCK_PROPS_TO_UPDATE, + ); + + const resultFragment = + metaMetricsController.state.fragments[SAMPLE_PERSISTED_EVENT.id]; + const expectedPartialFragment = { + ...SAMPLE_PERSISTED_EVENT, + ...MOCK_PROPS_TO_UPDATE, + lastUpdated: 1730798303333, + }; + expect(resultFragment).toStrictEqual(expectedPartialFragment); + }); + + it('throws error when no existing fragment exists', async function () { + const metaMetricsController = getMetaMetricsController(); + + const MOCK_NONEXISTING_ID = 'test-nonexistingid'; + + await expect(() => { + metaMetricsController.updateEventFragment(MOCK_NONEXISTING_ID, { + properties: { test: 1 }, + }); + }).toThrow(/Event fragment with id test-nonexistingid does not exist\./u); + }); + + describe('when id includes "transaction-submitted"', function () { + it('creates and stores new fragment props with canDeleteIfAbandoned set to true', function () { + const metaMetricsController = getMetaMetricsController(); + const MOCK_ID = 'transaction-submitted-1111'; + const MOCK_PROPS_TO_UPDATE = { + properties: { + test: 1, + }, + }; + + metaMetricsController.updateEventFragment( + MOCK_ID, + MOCK_PROPS_TO_UPDATE, + ); + + const resultFragment = metaMetricsController.state.fragments[MOCK_ID]; + const expectedPartialFragment = { + ...MOCK_PROPS_TO_UPDATE, + category: 'Transactions', + canDeleteIfAbandoned: true, + id: MOCK_ID, + lastUpdated: 1730798303333, + successEvent: 'Transaction Finalized', + }; + expect(resultFragment).toStrictEqual(expectedPartialFragment); + }); + }); + }); + describe('generateMetaMetricsId', function () { it('should generate an 0x prefixed hex string', function () { const metaMetricsController = getMetaMetricsController(); @@ -242,7 +482,7 @@ describe('MetaMetricsController', function () { describe('identify', function () { it('should call segment.identify for valid traits if user is participating in metametrics', function () { - const spy = jest.spyOn(segment, 'identify'); + const spy = jest.spyOn(segmentMock, 'identify'); const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: true, metaMetricsId: TEST_META_METRICS_ID, @@ -264,12 +504,14 @@ describe('MetaMetricsController', function () { }); it('should transform date type traits into ISO-8601 timestamp strings', function () { - const spy = jest.spyOn(segment, 'identify'); + const spy = jest.spyOn(segmentMock, 'identify'); const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: true, metaMetricsId: TEST_META_METRICS_ID, }); - metaMetricsController.identify({ test_date: new Date().toISOString() }); + metaMetricsController.identify({ + test_date: new Date().toISOString(), + } as MetaMetricsUserTraits); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith( { @@ -285,7 +527,7 @@ describe('MetaMetricsController', function () { }); it('should not call segment.identify if user is not participating in metametrics', function () { - const spy = jest.spyOn(segment, 'identify'); + const spy = jest.spyOn(segmentMock, 'identify'); const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }); @@ -294,7 +536,7 @@ describe('MetaMetricsController', function () { }); it('should not call segment.identify if there are no valid traits to identify', function () { - const spy = jest.spyOn(segment, 'identify'); + const spy = jest.spyOn(segmentMock, 'identify'); const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: true, metaMetricsId: TEST_META_METRICS_ID, @@ -359,7 +601,7 @@ describe('MetaMetricsController', function () { describe('submitEvent', function () { it('should not track an event if user is not participating in metametrics', function () { - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }); @@ -367,7 +609,7 @@ describe('MetaMetricsController', function () { event: 'Fake Event', category: 'Unit Test', properties: { - test: 1, + chain_id: '1', }, }); expect(spy).toHaveBeenCalledTimes(0); @@ -377,13 +619,13 @@ describe('MetaMetricsController', function () { const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: true, }); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent( { event: 'Fake Event', category: 'Unit Test', properties: { - test: 1, + chain_id: '1', }, }, { isOptIn: true }, @@ -395,8 +637,8 @@ describe('MetaMetricsController', function () { anonymousId: METAMETRICS_ANONYMOUS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - test: 1, ...DEFAULT_EVENT_PROPERTIES, + chain_id: '1', }, messageId: Utils.generateRandomId(), timestamp: new Date(), @@ -409,13 +651,13 @@ describe('MetaMetricsController', function () { const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: true, }); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent( { event: 'Fake Event', category: 'Unit Test', properties: { - test: 1, + chain_id: '1', }, }, { isOptIn: true, metaMetricsId: 'TESTID' }, @@ -427,8 +669,8 @@ describe('MetaMetricsController', function () { userId: 'TESTID', context: DEFAULT_TEST_CONTEXT, properties: { - test: 1, ...DEFAULT_EVENT_PROPERTIES, + chain_id: '1', }, messageId: Utils.generateRandomId(), timestamp: new Date(), @@ -439,13 +681,13 @@ describe('MetaMetricsController', function () { it('should track a legacy event', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent( { event: 'Fake Event', category: 'Unit Test', properties: { - test: 1, + chain_id: '1', }, }, { matomoEvent: true }, @@ -457,9 +699,9 @@ describe('MetaMetricsController', function () { userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - test: 1, - legacy_event: true, ...DEFAULT_EVENT_PROPERTIES, + legacy_event: true, + chain_id: '1', }, messageId: Utils.generateRandomId(), timestamp: new Date(), @@ -470,12 +712,12 @@ describe('MetaMetricsController', function () { it('should track a non legacy event', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', properties: { - test: 1, + chain_id: '1', }, }); expect(spy).toHaveBeenCalledTimes(1); @@ -483,8 +725,8 @@ describe('MetaMetricsController', function () { { event: 'Fake Event', properties: { - test: 1, ...DEFAULT_EVENT_PROPERTIES, + chain_id: '1', }, context: DEFAULT_TEST_CONTEXT, userId: TEST_META_METRICS_ID, @@ -497,7 +739,7 @@ describe('MetaMetricsController', function () { it('should immediately flush queue if flushImmediately set to true', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'flush'); + const spy = jest.spyOn(segmentMock, 'flush'); metaMetricsController.submitEvent( { event: 'Fake Event', @@ -512,10 +754,14 @@ describe('MetaMetricsController', function () { const metaMetricsController = getMetaMetricsController(); await expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error because we are testing the error case metaMetricsController.submitEvent({ event: 'test' }), ).rejects.toThrow(/Must specify event and category\./u); await expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error because we are testing the error case metaMetricsController.submitEvent({ category: 'test' }), ).rejects.toThrow(/Must specify event and category\./u); }); @@ -538,7 +784,7 @@ describe('MetaMetricsController', function () { it('should track sensitiveProperties in a separate, anonymous event', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', @@ -574,15 +820,16 @@ describe('MetaMetricsController', function () { }); describe('Change Signature XXX anonymous event names', function () { + // @ts-expect-error This function is missing from the Mocha type definitions it.each([ ['Signature Requested', 'Signature Requested Anon'], ['Signature Rejected', 'Signature Rejected Anon'], ['Signature Approved', 'Signature Approved Anon'], ])( 'should change "%s" anonymous event names to "%s"', - (eventType, anonEventType) => { + (eventType: string, anonEventType: string) => { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: eventType, category: 'Unit Test', @@ -608,7 +855,7 @@ describe('MetaMetricsController', function () { describe('Change Transaction XXX anonymous event namnes', function () { it('should change "Transaction Added" anonymous event names to "Transaction Added Anon"', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Transaction Added', category: 'Unit Test', @@ -633,7 +880,7 @@ describe('MetaMetricsController', function () { it('should change "Transaction Submitted" anonymous event names to "Transaction Added Anon"', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Transaction Submitted', category: 'Unit Test', @@ -658,7 +905,7 @@ describe('MetaMetricsController', function () { it('should change "Transaction Finalized" anonymous event names to "Transaction Added Anon"', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Transaction Finalized', category: 'Unit Test', @@ -685,10 +932,9 @@ describe('MetaMetricsController', function () { describe('trackPage', function () { it('should track a page view', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'page'); + const spy = jest.spyOn(segmentMock, 'page'); metaMetricsController.trackPage({ name: 'home', - params: null, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, }); @@ -699,7 +945,7 @@ describe('MetaMetricsController', function () { userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - params: null, + params: undefined, ...DEFAULT_PAGE_PROPERTIES, }, messageId: Utils.generateRandomId(), @@ -713,10 +959,9 @@ describe('MetaMetricsController', function () { const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }); - const spy = jest.spyOn(segment, 'page'); + const spy = jest.spyOn(segmentMock, 'page'); metaMetricsController.trackPage({ name: 'home', - params: null, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, }); @@ -725,17 +970,14 @@ describe('MetaMetricsController', function () { it('should track a page view if isOptInPath is true and user not yet opted in', function () { const metaMetricsController = getMetaMetricsController({ - preferencesControllerState: { - currentLocale: LOCALE, - participateInMetaMetrics: null, - }, + currentLocale: LOCALE, + participateInMetaMetrics: true, onPreferencesStateChange: jest.fn(), }); - const spy = jest.spyOn(segment, 'page'); + const spy = jest.spyOn(segmentMock, 'page'); metaMetricsController.trackPage( { name: 'home', - params: null, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, }, @@ -749,7 +991,6 @@ describe('MetaMetricsController', function () { userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - params: null, ...DEFAULT_PAGE_PROPERTIES, }, messageId: Utils.generateRandomId(), @@ -761,17 +1002,14 @@ describe('MetaMetricsController', function () { it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () { const metaMetricsController = getMetaMetricsController({ - preferencesControllerState: { - currentLocale: LOCALE, - participateInMetaMetrics: null, - }, + currentLocale: LOCALE, + participateInMetaMetrics: true, onPreferencesStateChange: jest.fn(), }); - const spy = jest.spyOn(segment, 'page'); + const spy = jest.spyOn(segmentMock, 'page'); metaMetricsController.trackPage( { name: 'home', - params: null, actionId: DUMMY_ACTION_ID, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, @@ -781,7 +1019,6 @@ describe('MetaMetricsController', function () { metaMetricsController.trackPage( { name: 'home', - params: null, actionId: DUMMY_ACTION_ID, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, @@ -795,10 +1032,7 @@ describe('MetaMetricsController', function () { name: 'home', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, - properties: { - params: null, - ...DEFAULT_PAGE_PROPERTIES, - }, + properties: DEFAULT_PAGE_PROPERTIES, messageId: DUMMY_ACTION_ID, timestamp: new Date(), }, @@ -810,11 +1044,13 @@ describe('MetaMetricsController', function () { describe('deterministic messageId', function () { it('should use the actionId as messageId when provided', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', - properties: { foo: 'bar' }, + properties: { + chain_id: 'bar', + }, actionId: '0x001', }); expect(spy).toHaveBeenCalledTimes(1); @@ -824,8 +1060,8 @@ describe('MetaMetricsController', function () { userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - foo: 'bar', ...DEFAULT_EVENT_PROPERTIES, + chain_id: 'bar', }, messageId: '0x001', timestamp: new Date(), @@ -836,7 +1072,7 @@ describe('MetaMetricsController', function () { it('should append 0x000 to the actionId of anonymized event when tracking sensitiveProperties', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', @@ -863,9 +1099,7 @@ describe('MetaMetricsController', function () { event: 'Fake Event', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, - properties: { - ...DEFAULT_EVENT_PROPERTIES, - }, + properties: DEFAULT_EVENT_PROPERTIES, messageId: '0x001', timestamp: new Date(), }, @@ -875,11 +1109,13 @@ describe('MetaMetricsController', function () { it('should use the uniqueIdentifier as messageId when provided', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', - properties: { foo: 'bar' }, + properties: { + chain_id: 'bar', + }, uniqueIdentifier: 'transaction-submitted-0000', }); expect(spy).toHaveBeenCalledTimes(1); @@ -889,8 +1125,8 @@ describe('MetaMetricsController', function () { userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - foo: 'bar', ...DEFAULT_EVENT_PROPERTIES, + chain_id: 'bar', }, messageId: 'transaction-submitted-0000', timestamp: new Date(), @@ -901,7 +1137,7 @@ describe('MetaMetricsController', function () { it('should append 0x000 to the uniqueIdentifier of anonymized event when tracking sensitiveProperties', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', @@ -940,11 +1176,11 @@ describe('MetaMetricsController', function () { it('should combine the uniqueIdentifier and actionId as messageId when both provided', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', - properties: { foo: 'bar' }, + properties: { chain_id: 'bar' }, actionId: '0x001', uniqueIdentifier: 'transaction-submitted-0000', }); @@ -955,8 +1191,8 @@ describe('MetaMetricsController', function () { userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { - foo: 'bar', ...DEFAULT_EVENT_PROPERTIES, + chain_id: 'bar', }, messageId: 'transaction-submitted-0000-0x001', timestamp: new Date(), @@ -967,7 +1203,7 @@ describe('MetaMetricsController', function () { it('should append 0x000 to the combined uniqueIdentifier and actionId of anonymized event when tracking sensitiveProperties', function () { const metaMetricsController = getMetaMetricsController(); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent({ event: 'Fake Event', category: 'Unit Test', @@ -1008,7 +1244,7 @@ describe('MetaMetricsController', function () { describe('_buildUserTraitsObject', function () { it('should return full user traits object on first call', function () { - const MOCK_ALL_TOKENS = { + const MOCK_ALL_TOKENS: TokensControllerState['allTokens'] = { [toHex(1)]: { '0x1235ce91d74254f29d4609f25932fe6d97bf4842': [ { @@ -1017,12 +1253,12 @@ describe('MetaMetricsController', function () { { address: '0xabc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', }, - ], + ] as Token[], '0xe364b0f9d1879e53e8183055c9d7dd2b7375d86b': [ { address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91', }, - ], + ] as Token[], }, [toHex(4)]: { '0x1235ce91d74254f29d4609f25932fe6d97bf4842': [ @@ -1032,15 +1268,26 @@ describe('MetaMetricsController', function () { { address: '0x12317F958D2ee523a2206206994597C13D831ec7', }, - ], + ] as Token[], }, }; const metaMetricsController = getMetaMetricsController(); const traits = metaMetricsController._buildUserTraitsObject({ addressBook: { - [CHAIN_IDS.MAINNET]: [{ address: '0x' }], - [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], + [CHAIN_IDS.MAINNET]: { + '0x': { + address: '0x', + } as AddressBookEntry, + }, + [CHAIN_IDS.GOERLI]: { + '0x': { + address: '0x', + } as AddressBookEntry, + '0x0': { + address: '0x0', + } as AddressBookEntry, + }, }, allNfts: { '0xac706cE8A9BF27Afecf080fB298d0ee13cfb978A': { @@ -1057,7 +1304,7 @@ describe('MetaMetricsController', function () { address: '0x7488d2ce5deb26db021285b50b661d655eb3d3d9', tokenId: '99', }, - ], + ] as Nft[], }, '0xe04AB39684A24D8D4124b114F3bd6FBEB779cacA': { [toHex(59)]: [ @@ -1065,7 +1312,7 @@ describe('MetaMetricsController', function () { address: '0x63d646bc7380562376d5de205123a57b1718184d', tokenId: '14', }, - ], + ] as Nft[], }, }, allTokens: MOCK_ALL_TOKENS, @@ -1076,48 +1323,41 @@ describe('MetaMetricsController', function () { ), internalAccounts: { accounts: { - mock1: {}, - mock2: {}, + mock1: {} as InternalAccount, + mock2: {} as InternalAccount, }, + selectedAccount: 'mock1', }, - identities: [{}, {}], - ledgerTransportType: 'web-hid', + ledgerTransportType: LedgerTransportTypes.webhid, openSeaEnabled: true, useNftDetection: false, securityAlertsEnabled: true, theme: 'default', useTokenDetection: true, - showNativeTokenAsMainBalance: true, + ShowNativeTokenAsMainBalance: true, security_providers: [], names: { [NameType.ETHEREUM_ADDRESS]: { '0x123': { '0x1': { name: 'Test 1', - }, + } as NameEntry, '0x2': { name: 'Test 2', - }, + } as NameEntry, '0x3': { name: null, - }, + } as NameEntry, }, '0x456': { '0x1': { name: 'Test 3', - }, + } as NameEntry, }, '0x789': { '0x1': { name: null, - }, - }, - }, - otherType: { - otherValue: { - otherVariation: { - name: 'Test 4', - }, + } as NameEntry, }, }, }, @@ -1126,12 +1366,19 @@ describe('MetaMetricsController', function () { order: 'dsc', sortCallback: 'stringNumeric', }, + participateInMetaMetrics: true, + currentCurrency: 'usd', + dataCollectionForMarketing: false, + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + custodyAccountDetails: {}, + ///: END:ONLY_INCLUDE_IF }); expect(traits).toStrictEqual({ [MetaMetricsUserTrait.AddressBookEntries]: 3, [MetaMetricsUserTrait.InstallDateExt]: '', - [MetaMetricsUserTrait.LedgerConnectionType]: 'web-hid', + [MetaMetricsUserTrait.LedgerConnectionType]: + LedgerTransportTypes.webhid, [MetaMetricsUserTrait.NetworksAdded]: [ CHAIN_IDS.MAINNET, CHAIN_IDS.GOERLI, @@ -1143,12 +1390,15 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.NumberOfNftCollections]: 3, [MetaMetricsUserTrait.NumberOfNfts]: 4, [MetaMetricsUserTrait.NumberOfTokens]: 5, - [MetaMetricsUserTrait.OpenseaApiEnabled]: true, + [MetaMetricsUserTrait.OpenSeaApiEnabled]: true, [MetaMetricsUserTrait.ThreeBoxEnabled]: false, [MetaMetricsUserTrait.Theme]: 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: true, [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: true, + [MetaMetricsUserTrait.CurrentCurrency]: 'usd', + [MetaMetricsUserTrait.HasMarketingConsent]: false, [MetaMetricsUserTrait.SecurityProviders]: ['blockaid'], + [MetaMetricsUserTrait.IsMetricsOptedIn]: true, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: 'testid', [MetaMetricsUserTrait.MmiAccountAddress]: null, @@ -1169,20 +1419,31 @@ describe('MetaMetricsController', function () { ); metaMetricsController._buildUserTraitsObject({ addressBook: { - [CHAIN_IDS.MAINNET]: [{ address: '0x' }], - [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], + [CHAIN_IDS.MAINNET]: { + '0x': { + address: '0x', + } as AddressBookEntry, + }, + [CHAIN_IDS.GOERLI]: { + '0x': { + address: '0x', + } as AddressBookEntry, + '0x0': { + address: '0x0', + } as AddressBookEntry, + }, }, allTokens: {}, ...networkState, - ledgerTransportType: 'web-hid', + ledgerTransportType: LedgerTransportTypes.webhid, openSeaEnabled: true, internalAccounts: { accounts: { - mock1: {}, - mock2: {}, + mock1: {} as InternalAccount, + mock2: {} as InternalAccount, }, + selectedAccount: 'mock1', }, - identities: [{}, {}], useNftDetection: false, theme: 'default', useTokenDetection: true, @@ -1191,30 +1452,56 @@ describe('MetaMetricsController', function () { order: 'dsc', sortCallback: 'stringNumeric', }, - showNativeTokenAsMainBalance: true, + ShowNativeTokenAsMainBalance: true, + allNfts: {}, + participateInMetaMetrics: true, + dataCollectionForMarketing: false, + securityAlertsEnabled: true, + names: { + ethereumAddress: {}, + }, + security_providers: ['blockaid'], + currentCurrency: 'usd', + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + custodyAccountDetails: {}, + ///: END:ONLY_INCLUDE_IF }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ addressBook: { - [CHAIN_IDS.MAINNET]: [{ address: '0x' }, { address: '0x1' }], - [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], + [CHAIN_IDS.MAINNET]: { + '0x': { + address: '0x', + } as AddressBookEntry, + '0x1': { + address: '0x1', + } as AddressBookEntry, + }, + [CHAIN_IDS.GOERLI]: { + '0x': { + address: '0x', + } as AddressBookEntry, + '0x0': { + address: '0x0', + } as AddressBookEntry, + }, }, allTokens: { [toHex(1)]: { - '0xabcde': [{ '0x12345': { address: '0xtestAddress' } }], + '0xabcde': [{ address: '0xtestAddress' } as Token], }, }, ...networkState, - ledgerTransportType: 'web-hid', + ledgerTransportType: LedgerTransportTypes.webhid, openSeaEnabled: false, internalAccounts: { accounts: { - mock1: {}, - mock2: {}, - mock3: {}, + mock1: {} as InternalAccount, + mock2: {} as InternalAccount, + mock3: {} as InternalAccount, }, + selectedAccount: 'mock1', }, - identities: [{}, {}, {}], useNftDetection: false, theme: 'default', useTokenDetection: true, @@ -1223,14 +1510,26 @@ describe('MetaMetricsController', function () { order: 'dsc', sortCallback: 'stringNumeric', }, - showNativeTokenAsMainBalance: false, + ShowNativeTokenAsMainBalance: false, + names: { + ethereumAddress: {}, + }, + security_providers: ['blockaid'], + currentCurrency: 'usd', + allNfts: {}, + participateInMetaMetrics: true, + dataCollectionForMarketing: false, + securityAlertsEnabled: true, + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + custodyAccountDetails: {}, + ///: END:ONLY_INCLUDE_IF }); expect(updatedTraits).toStrictEqual({ [MetaMetricsUserTrait.AddressBookEntries]: 4, [MetaMetricsUserTrait.NumberOfAccounts]: 3, [MetaMetricsUserTrait.NumberOfTokens]: 1, - [MetaMetricsUserTrait.OpenseaApiEnabled]: false, + [MetaMetricsUserTrait.OpenSeaApiEnabled]: false, [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: false, }); }); @@ -1243,20 +1542,31 @@ describe('MetaMetricsController', function () { ); metaMetricsController._buildUserTraitsObject({ addressBook: { - [CHAIN_IDS.MAINNET]: [{ address: '0x' }], - [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], + [CHAIN_IDS.MAINNET]: { + '0x': { + address: '0x', + } as AddressBookEntry, + }, + [CHAIN_IDS.GOERLI]: { + '0x': { + address: '0x', + } as AddressBookEntry, + '0x0': { + address: '0x0', + } as AddressBookEntry, + }, }, allTokens: {}, ...networkState, - ledgerTransportType: 'web-hid', + ledgerTransportType: LedgerTransportTypes.webhid, openSeaEnabled: true, internalAccounts: { accounts: { - mock1: {}, - mock2: {}, + mock1: {} as InternalAccount, + mock2: {} as InternalAccount, }, + selectedAccount: 'mock1', }, - identities: [{}, {}], useNftDetection: true, theme: 'default', useTokenDetection: true, @@ -1265,25 +1575,46 @@ describe('MetaMetricsController', function () { order: 'dsc', sortCallback: 'stringNumeric', }, - showNativeTokenAsMainBalance: true, + ShowNativeTokenAsMainBalance: true, + allNfts: {}, + names: { + ethereumAddress: {}, + }, + participateInMetaMetrics: true, + dataCollectionForMarketing: false, + securityAlertsEnabled: true, + security_providers: ['blockaid'], + currentCurrency: 'usd', + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + custodyAccountDetails: {}, + ///: END:ONLY_INCLUDE_IF }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ addressBook: { - [CHAIN_IDS.MAINNET]: [{ address: '0x' }], - [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], + [CHAIN_IDS.MAINNET]: { + '0x': { + address: '0x', + } as AddressBookEntry, + }, + [CHAIN_IDS.GOERLI]: { + '0x': { + address: '0x', + } as AddressBookEntry, + '0x0': { address: '0x0' } as AddressBookEntry, + }, }, allTokens: {}, ...networkState, - ledgerTransportType: 'web-hid', + ledgerTransportType: LedgerTransportTypes.webhid, openSeaEnabled: true, internalAccounts: { accounts: { - mock1: {}, - mock2: {}, + mock1: {} as InternalAccount, + mock2: {} as InternalAccount, }, + selectedAccount: 'mock1', }, - identities: [{}, {}], useNftDetection: true, theme: 'default', useTokenDetection: true, @@ -1292,7 +1623,19 @@ describe('MetaMetricsController', function () { order: 'dsc', sortCallback: 'stringNumeric', }, - showNativeTokenAsMainBalance: true, + ShowNativeTokenAsMainBalance: true, + allNfts: {}, + participateInMetaMetrics: true, + dataCollectionForMarketing: false, + names: { + ethereumAddress: {}, + }, + securityAlertsEnabled: true, + security_providers: ['blockaid'], + currentCurrency: 'usd', + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + custodyAccountDetails: {}, + ///: END:ONLY_INCLUDE_IF }); expect(updatedTraits).toStrictEqual(null); }); @@ -1301,23 +1644,24 @@ describe('MetaMetricsController', function () { describe('submitting segmentApiCalls to segment SDK', function () { it('should add event to store when submitting to SDK', function () { const metaMetricsController = getMetaMetricsController({}); - metaMetricsController.trackPage({}, { isOptIn: true }); + metaMetricsController.trackPage({}, { isOptInPath: true }); const { segmentApiCalls } = metaMetricsController.store.getState(); expect(Object.keys(segmentApiCalls).length > 0).toStrictEqual(true); }); it('should remove event from store when callback is invoked', function () { - const segmentInstance = createSegmentMock(2, 10000); - const stubFn = (_, cb) => { + const segmentInstance = createSegmentMock(2); + const stubFn = (...args: unknown[]) => { + const cb = args[1] as () => void; cb(); }; jest.spyOn(segmentInstance, 'track').mockImplementation(stubFn); jest.spyOn(segmentInstance, 'page').mockImplementation(stubFn); const metaMetricsController = getMetaMetricsController({ - segmentInstance, + segment: segmentInstance, }); - metaMetricsController.trackPage({}, { isOptIn: true }); + metaMetricsController.trackPage({}, { isOptInPath: true }); const { segmentApiCalls } = metaMetricsController.store.getState(); expect(Object.keys(segmentApiCalls).length === 0).toStrictEqual(true); }); @@ -1333,13 +1677,13 @@ describe('MetaMetricsController', function () { expect( metaMetricsController.state.marketingCampaignCookieId, ).toStrictEqual(TEST_GA_COOKIE_ID); - const spy = jest.spyOn(segment, 'track'); + const spy = jest.spyOn(segmentMock, 'track'); metaMetricsController.submitEvent( { event: 'Fake Event', category: 'Unit Test', properties: { - test: 1, + chain_id: '1', }, }, { isOptIn: true }, @@ -1354,8 +1698,8 @@ describe('MetaMetricsController', function () { marketingCampaignCookieId: TEST_GA_COOKIE_ID, }, properties: { - test: 1, ...DEFAULT_EVENT_PROPERTIES, + chain_id: '1', }, messageId: Utils.generateRandomId(), timestamp: new Date(), @@ -1383,7 +1727,7 @@ describe('MetaMetricsController', function () { }); afterEach(function () { // flush the queues manually after each test - segment.flush(); + segmentMock.flush(); jest.useRealTimers(); jest.restoreAllMocks(); }); diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.ts similarity index 60% rename from app/scripts/controllers/metametrics.js rename to app/scripts/controllers/metametrics.ts index aa5546ef7899..ded99dd917f4 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.ts @@ -11,13 +11,40 @@ import { import { ObservableStore } from '@metamask/obs-store'; import { bufferToHex, keccak } from 'ethereumjs-util'; import { v4 as uuidv4 } from 'uuid'; -import { NameType } from '@metamask/name-controller'; +import { NameControllerState, NameType } from '@metamask/name-controller'; +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { + getErrorMessage, + Hex, + isErrorWithMessage, + isErrorWithStack, +} from '@metamask/utils'; +import { NetworkState } from '@metamask/network-controller'; +import { Browser } from 'webextension-polyfill'; +import { + Nft, + NftControllerState, + TokensControllerState, +} from '@metamask/assets-controllers'; +import { captureException as sentryCaptureException } from '@sentry/browser'; +import { AddressBookControllerState } from '@metamask/address-book-controller'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { METAMETRICS_ANONYMOUS_ID, METAMETRICS_BACKGROUND_PAGE_OBJECT, + MetaMetricsEventCategory, MetaMetricsEventName, + MetaMetricsEventFragment, MetaMetricsUserTrait, + MetaMetricsUserTraits, + SegmentEventPayload, + MetaMetricsContext, + MetaMetricsEventPayload, + MetaMetricsEventOptions, + MetaMetricsPagePayload, + MetaMetricsPageOptions, + MetaMetricsPageObject, + MetaMetricsReferrerObject, } from '../../../shared/constants/metametrics'; import { SECOND } from '../../../shared/constants/time'; import { isManifestV3 } from '../../../shared/modules/mv3.utils'; @@ -27,11 +54,15 @@ import { AnonymousTransactionMetaMetricsEvent, TransactionMetaMetricsEvent, } from '../../../shared/constants/transaction'; +import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets'; +import Analytics from '../lib/segment/analytics'; ///: BEGIN:ONLY_INCLUDE_IF(build-main) import { ENVIRONMENT } from '../../../development/build/constants'; ///: END:ONLY_INCLUDE_IF +import type { PreferencesControllerState } from './preferences-controller'; + const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled'; export const overrideAnonymousEventNames = { @@ -51,9 +82,9 @@ export const overrideAnonymousEventNames = { MetaMetricsEventName.SignatureApprovedAnon, [MetaMetricsEventName.SignatureRejected]: MetaMetricsEventName.SignatureRejectedAnon, -}; +} as const; -const defaultCaptureException = (err) => { +const defaultCaptureException = (err: unknown) => { // throw error on clean stack so its captured by platform integrations (eg sentry) // but does not interrupt the call stack setTimeout(() => { @@ -63,7 +94,11 @@ const defaultCaptureException = (err) => { // The function is used to build a unique messageId for segment messages // It uses actionId and uniqueIdentifier from event if present -const buildUniqueMessageId = (args) => { +const buildUniqueMessageId = (args: { + uniqueIdentifier?: string; + actionId?: string; + isDuplicateAnonymizedEvent?: boolean; +}): string => { const messageIdParts = []; if (args.uniqueIdentifier) { messageIdParts.push(args.uniqueIdentifier); @@ -80,55 +115,134 @@ const buildUniqueMessageId = (args) => { return generateRandomId(); }; -const exceptionsToFilter = { +const exceptionsToFilter: Record = { [`You must pass either an "anonymousId" or a "userId".`]: true, }; /** - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsContext} MetaMetricsContext - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions - * @typedef {import('../../../shared/constants/metametrics').SegmentEventPayload} SegmentEventPayload - * @typedef {import('../../../shared/constants/metametrics').SegmentInterface} SegmentInterface - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventFragment} MetaMetricsEventFragment - * @typedef {import('../../../shared/constants/metametrics').MetaMetricsTraits} MetaMetricsTraits + * The type of a Segment event to create. + * + * Must correspond to the name of a method in {@link Analytics}. */ +type SegmentEventType = 'identify' | 'track' | 'page'; + +// TODO: Complete MetaMaskState by adding the full state definition and relocate it after the background is converted to TypeScript. +export type MetaMaskState = { + ledgerTransportType: LedgerTransportTypes; + networkConfigurationsByChainId: NetworkState['networkConfigurationsByChainId']; + internalAccounts: AccountsControllerState['internalAccounts']; + allNfts: NftControllerState['allNfts']; + allTokens: TokensControllerState['allTokens']; + theme: string; + participateInMetaMetrics: boolean; + dataCollectionForMarketing: boolean; + ShowNativeTokenAsMainBalance: boolean; + useNftDetection: PreferencesControllerState['useNftDetection']; + openSeaEnabled: PreferencesControllerState['openSeaEnabled']; + securityAlertsEnabled: PreferencesControllerState['securityAlertsEnabled']; + useTokenDetection: PreferencesControllerState['useTokenDetection']; + tokenSortConfig: PreferencesControllerState['preferences']['tokenSortConfig']; + names: NameControllerState['names']; + security_providers: string[]; + addressBook: AddressBookControllerState['addressBook']; + currentCurrency: string; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + custodyAccountDetails: { + [address: string]: { + custodianName: string; + }; + }; + ///: END:ONLY_INCLUDE_IF +}; /** - * @typedef {object} MetaMetricsControllerState - * @property {string} [metaMetricsId] - The user's metaMetricsId that will be - * attached to all non-anonymized event payloads - * @property {boolean} [participateInMetaMetrics] - The user's preference for - * participating in the MetaMetrics analytics program. This setting controls - * whether or not events are tracked - * @property {boolean} [latestNonAnonymousEventTimestamp] - The timestamp at which last non anonymous event is tracked. - * @property {{[string]: MetaMetricsEventFragment}} [fragments] - Object keyed - * by UUID with stored fragments as values. - * @property {Array} [eventsBeforeMetricsOptIn] - Array of queued events added before - * a user opts into metrics. - * @property {object} [traits] - Traits that are not derived from other state keys. - * @property {Record} [previousUserTraits] - The user traits the last - * time they were computed. + * The state that MetaMetricsController stores. + * + * @property metaMetricsId - The user's metaMetricsId that will be attached to all non-anonymized event payloads + * @property participateInMetaMetrics - The user's preference for participating in the MetaMetrics analytics program. + * This setting controls whether or not events are tracked + * @property latestNonAnonymousEventTimestamp - The timestamp at which last non anonymous event is tracked. + * @property fragments - Object keyed by UUID with stored fragments as values. + * @property eventsBeforeMetricsOptIn - Array of queued events added before a user opts into metrics. + * @property traits - Traits that are not derived from other state keys. + * @property previousUserTraits - The user traits the last time they were computed. + * @property dataCollectionForMarketing - Flag to determine if data collection for marketing is enabled. + * @property marketingCampaignCookieId - The marketing campaign cookie id. + * @property segmentApiCalls - Object keyed by messageId with segment event type and payload as values. */ +export type MetaMetricsControllerState = { + metaMetricsId: string | null; + participateInMetaMetrics: boolean | null; + latestNonAnonymousEventTimestamp: number; + fragments: Record; + eventsBeforeMetricsOptIn: MetaMetricsEventPayload[]; + traits: MetaMetricsUserTraits; + previousUserTraits?: MetaMetricsUserTraits; + dataCollectionForMarketing: boolean | null; + marketingCampaignCookieId: string | null; + segmentApiCalls: Record< + string, + { + eventType: SegmentEventType; + payload: SegmentEventPayload; + } + >; +}; + +type CaptureException = + | typeof sentryCaptureException + | ((err: unknown) => void); + +export type MetaMetricsControllerOptions = { + initState: Partial; + segment: Analytics; + preferencesControllerState: PreferencesControllerState; + onPreferencesStateChange: ( + listener: (state: PreferencesControllerState) => void, + ) => void; + onNetworkDidChange: (listener: (networkState: NetworkState) => void) => void; + getCurrentChainId: () => Hex; + version: string; + environment: string; + extension: Browser; + captureException?: CaptureException; +}; export default class MetaMetricsController { + store: ObservableStore; + + #captureException: CaptureException; + + chainId: Hex; + + locale: string; + + version: MetaMetricsControllerOptions['version']; + + #extension: MetaMetricsControllerOptions['extension']; + + #environment: MetaMetricsControllerOptions['environment']; + + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + #selectedAddress: PreferencesControllerState['selectedAddress']; + ///: END:ONLY_INCLUDE_IF + + #segment: MetaMetricsControllerOptions['segment']; + /** - * @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.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 - * network controller - * @param {string} options.version - The version of the extension - * @param {string} options.environment - The environment the extension is running in - * @param {string} options.extension - webextension-polyfill - * @param {MetaMetricsControllerState} options.initState - State to initialized with + * @param options + * @param options.segment - an instance of analytics for tracking + * events that conform to the new MetaMetrics tracking plan. + * @param options.preferencesControllerState - The state of preferences controller + * @param options.onPreferencesStateChange - Used to attach a listener to the + * stateChange event emitted by the PreferencesController + * @param options.onNetworkDidChange - Used to attach a listener to the + * networkDidChange event emitted by the networkController + * @param options.getCurrentChainId - Gets the current chain id from the network controller. + * @param options.version - The version of the extension + * @param options.environment - The environment the extension is running in + * @param options.extension - webextension-polyfill + * @param options.initState - State to initialized with * @param options.captureException */ constructor({ @@ -142,11 +256,12 @@ export default class MetaMetricsController { initState, extension, captureException = defaultCaptureException, - }) { - this._captureException = (err) => { + }: MetaMetricsControllerOptions) { + this.#captureException = (err: unknown) => { + const message = getErrorMessage(err); // This is a temporary measure. Currently there are errors flooding sentry due to a problem in how we are tracking anonymousId // We intend on removing this as soon as we understand how to correctly solve that problem. - if (!exceptionsToFilter[err.message]) { + if (!exceptionsToFilter[message]) { captureException(err); } }; @@ -154,11 +269,11 @@ export default class MetaMetricsController { this.locale = preferencesControllerState.currentLocale.replace('_', '-'); this.version = environment === 'production' ? version : `${version}-${environment}`; - this.extension = extension; - this.environment = environment; + this.#extension = extension; + this.#environment = environment; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - this.selectedAddress = preferencesControllerState.selectedAddress; + this.#selectedAddress = preferencesControllerState.selectedAddress; ///: END:ONLY_INCLUDE_IF const abandonedFragments = omitBy(initState?.fragments, 'persist'); @@ -189,7 +304,7 @@ export default class MetaMetricsController { onNetworkDidChange(() => { this.chainId = getCurrentChainId(); }); - this.segment = segment; + this.#segment = segment; // Track abandoned fragments that weren't properly cleaned up. // Abandoned fragments are those that were stored in persistent memory @@ -198,16 +313,16 @@ export default class MetaMetricsController { // fragments that are not marked as persistent will be purged and the // failure event will be emitted. Object.values(abandonedFragments).forEach((fragment) => { - this.finalizeEventFragment(fragment.id, { abandoned: true }); + this.processAbandonedFragment(fragment); }); // Code below submits any pending segmentApiCalls to Segment if/when the controller is re-instantiated if (isManifestV3) { Object.values(segmentApiCalls).forEach(({ eventType, payload }) => { try { - this._submitSegmentAPICall(eventType, payload); + this.#submitSegmentAPICall(eventType, payload); } catch (error) { - this._captureException(error); + this.#captureException(error); } }); } @@ -219,14 +334,14 @@ export default class MetaMetricsController { // tracked if the event isn't progressed within that amount of time. if (isManifestV3) { /* eslint-disable no-undef */ - this.extension.alarms.getAll().then((alarms) => { + this.#extension.alarms.getAll().then((alarms) => { const hasAlarm = checkAlarmExists( alarms, METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM, ); if (!hasAlarm) { - this.extension.alarms.create( + this.#extension.alarms.create( METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM, { delayInMinutes: 1, @@ -235,7 +350,7 @@ export default class MetaMetricsController { ); } }); - this.extension.alarms.onAlarm.addListener((alarmInfo) => { + this.#extension.alarms.onAlarm.addListener((alarmInfo) => { if (alarmInfo.name === METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM) { this.finalizeAbandonedFragments(); } @@ -247,18 +362,19 @@ export default class MetaMetricsController { } } - finalizeAbandonedFragments() { + finalizeAbandonedFragments(): void { Object.values(this.store.getState().fragments).forEach((fragment) => { if ( fragment.timeout && + fragment.lastUpdated && Date.now() - fragment.lastUpdated / 1000 > fragment.timeout ) { - this.finalizeEventFragment(fragment.id, { abandoned: true }); + this.processAbandonedFragment(fragment); } }); } - generateMetaMetricsId() { + generateMetaMetricsId(): string { return bufferToHex( keccak( Buffer.from( @@ -272,11 +388,11 @@ export default class MetaMetricsController { /** * Create an event fragment in state and returns the event fragment object. * - * @param {MetaMetricsEventFragment} options - Fragment settings and properties - * to initiate the fragment with. - * @returns {MetaMetricsEventFragment} + * @param options - Fragment settings and properties to initiate the fragment with. */ - createEventFragment(options) { + createEventFragment( + options: Omit, + ): MetaMetricsEventFragment { if (!options.successEvent || !options.category) { throw new Error( `Must specify success event and category. Success event was: ${ @@ -299,14 +415,34 @@ export default class MetaMetricsController { ...options, lastUpdated: Date.now(), }; + + /** + * HACK: "transaction-submitted-" fragment hack + * A "transaction-submitted-" fragment may exist following the "Transaction Added" + * event to persist accumulated event fragment props to the "Transaction Submitted" event + * which fires after a user confirms a transaction. Rejecting a confirmation does not fire the + * "Transaction Submitted" event. In this case, these abandoned fragments will be deleted + * instead of finalized with canDeleteIfAbandoned set to true. + */ + const hasExistingSubmittedFragment = + options.initialEvent === TransactionMetaMetricsEvent.submitted && + fragments[id]; + + const additionalFragmentProps = hasExistingSubmittedFragment + ? { + ...fragments[id], + canDeleteIfAbandoned: false, + } + : {}; + this.store.updateState({ fragments: { ...fragments, - [id]: fragment, + [id]: merge(additionalFragmentProps, fragment), }, }); - if (options.initialEvent) { + if (fragment.initialEvent) { this.trackEvent({ event: fragment.initialEvent, category: fragment.category, @@ -330,10 +466,9 @@ export default class MetaMetricsController { * Returns the fragment stored in memory with provided id or undefined if it * does not exist. * - * @param {string} id - id of fragment to retrieve - * @returns {[MetaMetricsEventFragment]} + * @param id - id of fragment to retrieve */ - getEventFragmentById(id) { + getEventFragmentById(id: string): MetaMetricsEventFragment { const { fragments } = this.store.getState(); const fragment = fragments[id]; @@ -341,19 +476,49 @@ export default class MetaMetricsController { return fragment; } + /** + * Deletes to finalizes event fragment based on the canDeleteIfAbandoned property. + * + * @param fragment + */ + processAbandonedFragment(fragment: MetaMetricsEventFragment): void { + if (fragment.canDeleteIfAbandoned) { + this.deleteEventFragment(fragment.id); + } else { + this.finalizeEventFragment(fragment.id, { abandoned: true }); + } + } + /** * Updates an event fragment in state * - * @param {string} id - The fragment id to update - * @param {Partial} payload - Fragment settings and - * properties to initiate the fragment with. + * @param id - The fragment id to update + * @param payload - Fragment settings and properties to initiate the fragment with. */ - updateEventFragment(id, payload) { + updateEventFragment( + id: string, + payload: Partial, + ): void { const { fragments } = this.store.getState(); const fragment = fragments[id]; - if (!fragment) { + /** + * HACK: "transaction-submitted-" fragment hack + * Creates a "transaction-submitted-" fragment if it does not exist to persist + * accumulated event metrics. In the case it is unused, the abandoned fragment will + * eventually be deleted with canDeleteIfAbandoned set to true. + */ + const createIfNotFound = !fragment && id.includes('transaction-submitted-'); + + if (createIfNotFound) { + fragments[id] = { + canDeleteIfAbandoned: true, + category: MetaMetricsEventCategory.Transactions, + successEvent: TransactionMetaMetricsEvent.finalized, + id, + }; + } else if (!fragment) { throw new Error(`Event fragment with id ${id} does not exist.`); } @@ -369,24 +534,41 @@ export default class MetaMetricsController { } /** - * @typedef {object} MetaMetricsFinalizeEventFragmentOptions - * @property {boolean} [abandoned = false] - if true track the failure - * event instead of the success event - * @property {MetaMetricsContext.page} [page] - page the final event - * occurred on. This will override whatever is set on the fragment - * @property {MetaMetricsContext.referrer} [referrer] - Dapp that - * originated the fragment. This is for fallback only, the fragment referrer - * property will take precedence. + * Deletes an event fragment from state + * + * @param id - The fragment id to delete */ + deleteEventFragment(id: string): void { + const { fragments } = this.store.getState(); + + if (fragments[id]) { + delete fragments[id]; + } + } /** * Finalizes a fragment, tracking either a success event or failure Event * and then removes the fragment from state. * - * @param {string} id - UUID of the event fragment to be closed - * @param {MetaMetricsFinalizeEventFragmentOptions} options + * @param id - UUID of the event fragment to be closed + * @param options + * @param options.abandoned - if true track the failure event instead of the success event + * @param options.page - page the final event occurred on. This will override whatever is set on the fragment + * @param options.referrer - Dapp that originated the fragment. This is for fallback only, the fragment referrer + * property will take precedence. */ - finalizeEventFragment(id, { abandoned = false, page, referrer } = {}) { + finalizeEventFragment( + id: string, + { + abandoned = false, + page, + referrer, + }: { + abandoned?: boolean; + page?: MetaMetricsPageObject; + referrer?: MetaMetricsReferrerObject; + } = {}, + ): void { const fragment = this.store.getState().fragments[id]; if (!fragment) { throw new Error(`Funnel with id ${id} does not exist.`); @@ -395,7 +577,7 @@ export default class MetaMetricsController { const eventName = abandoned ? fragment.failureEvent : fragment.successEvent; this.trackEvent({ - event: eventName, + event: eventName ?? '', category: fragment.category, properties: fragment.properties, sensitiveProperties: fragment.sensitiveProperties, @@ -424,9 +606,9 @@ export default class MetaMetricsController { * Calls this._identify with validated metaMetricsId and user traits if user is participating * in the MetaMetrics analytics program * - * @param {object} userTraits + * @param userTraits */ - identify(userTraits) { + identify(userTraits: Partial): void { const { metaMetricsId, participateInMetaMetrics } = this.state; if (!participateInMetaMetrics || !metaMetricsId || !userTraits) { @@ -439,26 +621,33 @@ export default class MetaMetricsController { return; } - const allValidTraits = this._buildValidTraits(userTraits); + const allValidTraits = this.#buildValidTraits(userTraits); - this._identify(allValidTraits); + this.#identify(allValidTraits); } // It sets an uninstall URL ("Sorry to see you go!" page), // which is opened if a user uninstalls the extension. - updateExtensionUninstallUrl(participateInMetaMetrics, metaMetricsId) { - const query = {}; + updateExtensionUninstallUrl( + participateInMetaMetrics: boolean, + metaMetricsId: string, + ): void { + const query: { + mmi?: string; + env?: string; + av?: string; + } = {}; if (participateInMetaMetrics) { // We only want to track these things if a user opted into metrics. query.mmi = Buffer.from(metaMetricsId).toString('base64'); - query.env = this.environment; + query.env = this.#environment; query.av = this.version; } const queryString = new URLSearchParams(query); // this.extension not currently defined in tests - if (this.extension && this.extension.runtime) { - this.extension.runtime.setUninstallURL( + if (this.#extension && this.#extension.runtime) { + this.#extension.runtime.setUninstallURL( `${EXTENSION_UNINSTALL_URL}?${queryString}`, ); } @@ -467,12 +656,12 @@ export default class MetaMetricsController { /** * Setter for the `participateInMetaMetrics` property * - * @param {boolean} participateInMetaMetrics - Whether or not the user wants - * to participate in MetaMetrics - * @returns {Promise} the string of the new metametrics id, or null - * if not set + * @param participateInMetaMetrics - Whether or not the user wants to participate in MetaMetrics if not set + * @returns The string of the new metametrics id, or null */ - async setParticipateInMetaMetrics(participateInMetaMetrics) { + async setParticipateInMetaMetrics( + participateInMetaMetrics: boolean, + ): Promise { const { metaMetricsId: existingMetaMetricsId } = this.state; const metaMetricsId = @@ -490,7 +679,10 @@ export default class MetaMetricsController { } ///: BEGIN:ONLY_INCLUDE_IF(build-main) - if (this.environment !== ENVIRONMENT.DEVELOPMENT) { + if ( + this.#environment !== ENVIRONMENT.DEVELOPMENT && + metaMetricsId !== null + ) { this.updateExtensionUninstallUrl(participateInMetaMetrics, metaMetricsId); } ///: END:ONLY_INCLUDE_IF @@ -498,7 +690,9 @@ export default class MetaMetricsController { return metaMetricsId; } - setDataCollectionForMarketing(dataCollectionForMarketing) { + setDataCollectionForMarketing( + dataCollectionForMarketing: boolean, + ): MetaMetricsControllerState['metaMetricsId'] { const { metaMetricsId } = this.state; this.store.updateState({ dataCollectionForMarketing }); @@ -510,25 +704,24 @@ export default class MetaMetricsController { return metaMetricsId; } - setMarketingCampaignCookieId(marketingCampaignCookieId) { + setMarketingCampaignCookieId(marketingCampaignCookieId: string | null): void { this.store.updateState({ marketingCampaignCookieId }); } - get state() { + get state(): MetaMetricsControllerState { return this.store.getState(); } /** * track a page view with Segment * - * @param {MetaMetricsPagePayload} payload - details of the page viewed - * @param {MetaMetricsPageOptions} [options] - options for handling the page - * view + * @param payload - details of the page viewed. + * @param options - options for handling the page view. */ trackPage( - { name, params, environmentType, page, referrer, actionId }, - options, - ) { + payload: MetaMetricsPagePayload, + options?: MetaMetricsPageOptions, + ): void { try { if (this.state.participateInMetaMetrics === false) { return; @@ -540,10 +733,13 @@ export default class MetaMetricsController { ) { return; } + + const { name, params, environmentType, page, referrer, actionId } = + payload; const { metaMetricsId } = this.state; const idTrait = metaMetricsId ? 'userId' : 'anonymousId'; const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID; - this._submitSegmentAPICall('page', { + this.#submitSegmentAPICall('page', { messageId: buildUniqueMessageId({ actionId }), [idTrait]: idValue, name, @@ -553,24 +749,27 @@ export default class MetaMetricsController { chain_id: this.chainId, environment_type: environmentType, }, - context: this._buildContext(referrer, page), + context: this.#buildContext(referrer, page), }); } catch (err) { - this._captureException(err); + this.#captureException(err); } } /** * submits a metametrics event, not waiting for it to complete or allowing its error to bubble up * - * @param {MetaMetricsEventPayload} payload - details of the event - * @param {MetaMetricsEventOptions} [options] - options for handling/routing the event + * @param payload - details of the event + * @param options - options for handling/routing the event */ - trackEvent(payload, options) { + trackEvent( + payload: MetaMetricsEventPayload, + options?: MetaMetricsEventOptions, + ): void { // validation is not caught and handled this.validatePayload(payload); this.submitEvent(payload, options).catch((err) => - this._captureException(err), + this.#captureException(err), ); } @@ -580,11 +779,13 @@ export default class MetaMetricsController { * with sensitiveProperties into two events, tracking the sensitiveProperties * with the anonymousId only. * - * @param {MetaMetricsEventPayload} payload - details of the event - * @param {MetaMetricsEventOptions} [options] - options for handling/routing the event - * @returns {Promise} + * @param payload - details of the event + * @param options - options for handling/routing the event */ - async submitEvent(payload, options) { + async submitEvent( + payload: MetaMetricsEventPayload, + options?: MetaMetricsEventOptions, + ): Promise { this.validatePayload(payload); if (!this.state.participateInMetaMetrics && !options?.isOptIn) { @@ -607,6 +808,7 @@ export default class MetaMetricsController { // change anonymous event names const anonymousEventName = + // @ts-expect-error This property may not exist. We check for it below. overrideAnonymousEventNames[`${payload.event}`]; const anonymousPayload = { ...payload, @@ -619,8 +821,8 @@ export default class MetaMetricsController { ); events.push( - this._track( - this._buildEventPayload({ + this.#track( + this.#buildEventPayload({ ...anonymousPayload, properties: combinedProperties, isDuplicateAnonymizedEvent: true, @@ -630,7 +832,7 @@ export default class MetaMetricsController { ); } - events.push(this._track(this._buildEventPayload(payload), options)); + events.push(this.#track(this.#buildEventPayload(payload), options)); await Promise.all(events); } @@ -638,9 +840,9 @@ export default class MetaMetricsController { /** * validates a metametrics event * - * @param {MetaMetricsEventPayload} payload - details of the event + * @param payload - details of the event */ - validatePayload(payload) { + validatePayload(payload: MetaMetricsEventPayload): void { // event and category are required fields for all payloads if (!payload.event || !payload.category) { throw new Error( @@ -657,7 +859,7 @@ export default class MetaMetricsController { } } - handleMetaMaskStateUpdate(newState) { + handleMetaMaskStateUpdate(newState: MetaMaskState): void { const userTraits = this._buildUserTraitsObject(newState); if (userTraits) { this.identify(userTraits); @@ -665,7 +867,7 @@ export default class MetaMetricsController { } // Track all queued events after a user opted into metrics. - trackEventsAfterMetricsOptIn() { + trackEventsAfterMetricsOptIn(): void { const { eventsBeforeMetricsOptIn } = this.store.getState(); eventsBeforeMetricsOptIn.forEach((eventBeforeMetricsOptIn) => { this.trackEvent(eventBeforeMetricsOptIn); @@ -673,14 +875,14 @@ export default class MetaMetricsController { } // Once we track queued events after a user opts into metrics, we want to clear the event queue. - clearEventsAfterMetricsOptIn() { + clearEventsAfterMetricsOptIn(): void { this.store.updateState({ eventsBeforeMetricsOptIn: [], }); } // It adds an event into a queue, which is only tracked if a user opts into metrics. - addEventBeforeMetricsOptIn(event) { + addEventBeforeMetricsOptIn(event: MetaMetricsEventPayload): void { const prevState = this.store.getState().eventsBeforeMetricsOptIn; this.store.updateState({ eventsBeforeMetricsOptIn: [...prevState, event], @@ -688,7 +890,7 @@ export default class MetaMetricsController { } // Add or update traits for tracking. - updateTraits(newTraits) { + updateTraits(newTraits: MetaMetricsUserTraits): void { const { traits } = this.store.getState(); this.store.updateState({ traits: { ...traits, ...newTraits }, @@ -696,7 +898,7 @@ export default class MetaMetricsController { } // Retrieve (or generate if doesn't exist) the client metametrics id - getMetaMetricsId() { + getMetaMetricsId(): string { let { metaMetricsId } = this.state; if (!metaMetricsId) { metaMetricsId = this.generateMetaMetricsId(); @@ -711,18 +913,22 @@ export default class MetaMetricsController { * Build the context object to attach to page and track events. * * @private - * @param {Pick} [referrer] - dapp origin that initialized - * the notification window. - * @param {Pick} [page] - page object describing the current - * view of the extension. Defaults to the background-process object. - * @returns {MetaMetricsContext} + * @param referrer - dapp origin that initialized + * the notification window. + * @param page - page object describing the current + * view of the extension. Defaults to the background-process object. */ - _buildContext(referrer, page = METAMETRICS_BACKGROUND_PAGE_OBJECT) { + #buildContext( + referrer: MetaMetricsContext['referrer'], + page: MetaMetricsContext['page'] = METAMETRICS_BACKGROUND_PAGE_OBJECT, + ): MetaMetricsContext { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - const mmiProps = {}; + const mmiProps: { + extensionId?: string; + } = {}; - if (this.extension?.runtime?.id) { - mmiProps.extensionId = this.extension.runtime.id; + if (this.#extension?.runtime?.id) { + mmiProps.extensionId = this.#extension.runtime.id; } ///: END:ONLY_INCLUDE_IF @@ -746,12 +952,12 @@ export default class MetaMetricsController { * fed to Segment's track method * * @private - * @param { - * Omit - * } rawPayload - raw payload provided to trackEvent - * @returns {SegmentEventPayload} formatted event payload for segment + * @param rawPayload - raw payload provided to trackEvent + * @returns formatted event payload for segment */ - _buildEventPayload(rawPayload) { + #buildEventPayload( + rawPayload: Omit, + ): SegmentEventPayload { const { event, properties, @@ -765,14 +971,17 @@ export default class MetaMetricsController { } = rawPayload; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - const mmiProps = {}; + const mmiProps: { + extensionId?: string; + accountAddress?: string; + } = {}; - if (this.extension?.runtime?.id) { - mmiProps.extensionId = this.extension.runtime.id; + if (this.#extension?.runtime?.id) { + mmiProps.extensionId = this.#extension.runtime.id; } - if (this.selectedAddress) { - mmiProps.accountAddress = this.selectedAddress; + if (this.#selectedAddress) { + mmiProps.accountAddress = this.#selectedAddress; } ///: END:ONLY_INCLUDE_IF @@ -792,13 +1001,18 @@ export default class MetaMetricsController { currency, category, locale: this.locale, - chain_id: properties?.chain_id ?? this.chainId, + chain_id: + properties && + 'chain_id' in properties && + typeof properties.chain_id === 'string' + ? properties.chain_id + : this.chainId, environment_type: environmentType, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) ...mmiProps, ///: END:ONLY_INCLUDE_IF }, - context: this._buildContext(referrer, page), + context: this.#buildContext(referrer, page), }; } @@ -806,10 +1020,12 @@ export default class MetaMetricsController { * This method generates the MetaMetrics user traits object, omitting any * traits that have not changed since the last invocation of this method. * - * @param {object} metamaskState - Full metamask state object. - * @returns {MetaMetricsTraits | null} traits that have changed since last update + * @param metamaskState - Full metamask state object. + * @returns traits that have changed since last update */ - _buildUserTraitsObject(metamaskState) { + _buildUserTraitsObject( + metamaskState: MetaMaskState, + ): Partial | null { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const mmiAccountAddress = metamaskState.custodyAccountDetails && @@ -819,10 +1035,11 @@ export default class MetaMetricsController { ///: END:ONLY_INCLUDE_IF const { traits, previousUserTraits } = this.store.getState(); - /** @type {MetaMetricsTraits} */ const currentTraits = { [MetaMetricsUserTrait.AddressBookEntries]: sum( - Object.values(metamaskState.addressBook).map(size), + Object.values(metamaskState.addressBook).map((v) => + size(v as object | string | null | undefined), + ), ), [MetaMetricsUserTrait.InstallDateExt]: traits[MetaMetricsUserTrait.InstallDateExt] || '', @@ -842,29 +1059,30 @@ export default class MetaMetricsController { metamaskState.internalAccounts.accounts, ).length, [MetaMetricsUserTrait.NumberOfNftCollections]: - this._getAllUniqueNFTAddressesLength(metamaskState.allNfts), - [MetaMetricsUserTrait.NumberOfNfts]: this._getAllNFTsFlattened( + this.#getAllUniqueNFTAddressesLength(metamaskState.allNfts), + [MetaMetricsUserTrait.NumberOfNfts]: this.#getAllNFTsFlattened( metamaskState.allNfts, ).length, - [MetaMetricsUserTrait.NumberOfTokens]: - this._getNumberOfTokens(metamaskState), - [MetaMetricsUserTrait.OpenseaApiEnabled]: metamaskState.openSeaEnabled, + [MetaMetricsUserTrait.NumberOfTokens]: this.#getNumberOfTokens( + metamaskState.allTokens, + ), + [MetaMetricsUserTrait.OpenSeaApiEnabled]: metamaskState.openSeaEnabled, [MetaMetricsUserTrait.ThreeBoxEnabled]: false, // deprecated, hard-coded as false [MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: metamaskState.useTokenDetection, [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: - metamaskState.showNativeTokenAsMainBalance, + metamaskState.ShowNativeTokenAsMainBalance, [MetaMetricsUserTrait.CurrentCurrency]: metamaskState.currentCurrency, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - [MetaMetricsUserTrait.MmiExtensionId]: this.extension?.runtime?.id, - [MetaMetricsUserTrait.MmiAccountAddress]: mmiAccountAddress, + [MetaMetricsUserTrait.MmiExtensionId]: this.#extension?.runtime?.id, + [MetaMetricsUserTrait.MmiAccountAddress]: mmiAccountAddress ?? null, [MetaMetricsUserTrait.MmiIsCustodian]: Boolean(mmiAccountAddress), ///: END:ONLY_INCLUDE_IF [MetaMetricsUserTrait.SecurityProviders]: metamaskState.securityAlertsEnabled ? ['blockaid'] : [], [MetaMetricsUserTrait.PetnameAddressCount]: - this._getPetnameAddressCount(metamaskState), + this.#getPetnameAddressCount(metamaskState), [MetaMetricsUserTrait.IsMetricsOptedIn]: metamaskState.participateInMetaMetrics, [MetaMetricsUserTrait.HasMarketingConsent]: @@ -874,15 +1092,18 @@ export default class MetaMetricsController { }; if (!previousUserTraits) { - this.store.updateState({ previousUserTraits: currentTraits }); + this.store.updateState({ + previousUserTraits: currentTraits, + }); return currentTraits; } if (previousUserTraits && !isEqual(previousUserTraits, currentTraits)) { - const updates = pickBy( - currentTraits, - (v, k) => !isEqual(previousUserTraits[k], v), - ); + const updates = pickBy(currentTraits, (v, k) => { + // @ts-expect-error It's okay that `k` may not be a key of `previousUserTraits`, because we assume `isEqual` can handle it + const previous = previousUserTraits[k]; + return !isEqual(previous, v); + }); this.store.updateState({ previousUserTraits: currentTraits }); return updates; } @@ -894,33 +1115,42 @@ export default class MetaMetricsController { * Returns a new object of all valid user traits. For dates, we transform them into ISO-8601 timestamp strings. * * @see {@link https://segment.com/docs/connections/spec/common/#timestamps} - * @param {object} userTraits - * @returns {object} + * @param userTraits */ - _buildValidTraits(userTraits) { - return Object.entries(userTraits).reduce((validTraits, [key, value]) => { - if (this._isValidTraitDate(value)) { - validTraits[key] = value.toISOString(); - } else if (this._isValidTrait(value)) { - validTraits[key] = value; - } else { + #buildValidTraits( + userTraits: Partial, + ): MetaMetricsUserTraits { + return Object.entries(userTraits).reduce( + (validTraits: MetaMetricsUserTraits, [key, value]) => { + if (this.#isValidTraitDate(value)) { + return { + ...validTraits, + [key]: value.toISOString(), + }; + } else if (this.#isValidTrait(value)) { + return { + ...validTraits, + [key]: value, + }; + } + console.warn( `MetaMetricsController: "${key}" value is not a valid trait type`, ); - } - return validTraits; - }, {}); + return validTraits; + }, + {}, + ); } /** * Returns an array of all of the NFTs the user * possesses across all networks and accounts. * - * @param {object} allNfts - * @returns {[]} + * @param allNfts */ - _getAllNFTsFlattened = memoize((allNfts = {}) => { - return Object.values(allNfts).reduce((result, chainNFTs) => { + #getAllNFTsFlattened = memoize((allNfts: MetaMaskState['allNfts'] = {}) => { + return Object.values(allNfts).reduce((result: Nft[], chainNFTs) => { return result.concat(...Object.values(chainNFTs)); }, []); }); @@ -929,11 +1159,12 @@ export default class MetaMetricsController { * Returns the number of unique NFT addresses the user * possesses across all networks and accounts. * - * @param {object} allNfts - * @returns {number} + * @param allNfts */ - _getAllUniqueNFTAddressesLength(allNfts = {}) { - const allNFTAddresses = this._getAllNFTsFlattened(allNfts).map( + #getAllUniqueNFTAddressesLength( + allNfts: MetaMaskState['allNfts'] = {}, + ): number { + const allNFTAddresses = this.#getAllNFTsFlattened(allNfts).map( (nft) => nft.address, ); const uniqueAddresses = new Set(allNFTAddresses); @@ -941,26 +1172,22 @@ export default class MetaMetricsController { } /** - * @param {object} metamaskState + * @param allTokens * @returns number of unique token addresses */ - _getNumberOfTokens(metamaskState) { - return Object.values(metamaskState.allTokens).reduce( - (result, accountsByChain) => { - return result + sum(Object.values(accountsByChain).map(size)); - }, - 0, - ); + #getNumberOfTokens(allTokens: MetaMaskState['allTokens']): number { + return Object.values(allTokens).reduce((result, accountsByChain) => { + return result + sum(Object.values(accountsByChain).map(size)); + }, 0); } /** * Calls segment.identify with given user traits * * @see {@link https://segment.com/docs/connections/sources/catalog/libraries/server/node/#identify} - * @private - * @param {object} userTraits + * @param userTraits */ - _identify(userTraits) { + #identify(userTraits: MetaMetricsUserTraits): void { const { metaMetricsId } = this.state; if (!userTraits || Object.keys(userTraits).length === 0) { @@ -969,12 +1196,12 @@ export default class MetaMetricsController { } try { - this._submitSegmentAPICall('identify', { - userId: metaMetricsId, + this.#submitSegmentAPICall('identify', { + userId: metaMetricsId ?? undefined, traits: userTraits, }); } catch (err) { - this._captureException(err); + this.#captureException(err); } } @@ -982,28 +1209,26 @@ export default class MetaMetricsController { * Validates the trait value. Segment accepts any data type. We are adding validation here to * support data types for our Segment destination(s) e.g. MixPanel * - * @param {*} value - * @returns {boolean} + * @param value */ - _isValidTrait(value) { + #isValidTrait(value: unknown): boolean { const type = typeof value; return ( type === 'string' || type === 'boolean' || type === 'number' || - this._isValidTraitArray(value) || - this._isValidTraitDate(value) + this.#isValidTraitArray(value) || + this.#isValidTraitDate(value) ); } /** * Segment accepts any data type value. We have special logic to validate arrays. * - * @param {*} value - * @returns {boolean} + * @param value */ - _isValidTraitArray = (value) => { + #isValidTraitArray(value: unknown): boolean { return ( Array.isArray(value) && (value.every((element) => { @@ -1016,17 +1241,16 @@ export default class MetaMetricsController { return typeof element === 'number'; })) ); - }; + } /** * Returns true if the value is an accepted date type * - * @param {*} value - * @returns {boolean} + * @param value */ - _isValidTraitDate = (value) => { + #isValidTraitDate(value: unknown): value is Date { return Object.prototype.toString.call(value) === '[object Date]'; - }; + } /** * Perform validation on the payload and update the id type to use before @@ -1034,19 +1258,20 @@ export default class MetaMetricsController { * event appropriately. * * @private - * @param {SegmentEventPayload} payload - properties to attach to event - * @param {MetaMetricsEventOptions} [options] - options for routing and - * handling the event - * @returns {Promise} + * @param payload - properties to attach to event + * @param options - options for routing and handling the event */ - _track(payload, options) { + #track( + payload: SegmentEventPayload, + options?: MetaMetricsEventOptions, + ): Promise { const { isOptIn, metaMetricsId: metaMetricsIdOverride, matomoEvent, flushImmediately, } = options || {}; - let idType = 'userId'; + let idType: 'userId' | 'anonymousId' = 'userId'; let idValue = this.state.metaMetricsId; let excludeMetaMetricsId = options?.excludeMetaMetricsId ?? false; // This is carried over from the old implementation, and will likely need @@ -1073,7 +1298,7 @@ export default class MetaMetricsController { } else if (isOptIn && metaMetricsIdOverride) { idValue = metaMetricsIdOverride; } - payload[idType] = idValue; + payload[idType] = idValue ?? undefined; // If this is an event on the old matomo schema, add a key to the payload // to designate it as such @@ -1085,33 +1310,43 @@ export default class MetaMetricsController { // event that relies on this promise being fulfilled before performing UI // updates, or otherwise delaying user interaction, supply the // 'flushImmediately' flag to the trackEvent method. - return new Promise((resolve, reject) => { - const callback = (err) => { + return new Promise((resolve, reject) => { + const callback = (err: unknown) => { if (err) { + const message = isErrorWithMessage(err) ? err.message : ''; + const stack = isErrorWithStack(err) ? err.stack : undefined; // The error that segment gives us has some manipulation done to it // that seemingly breaks with lockdown enabled. Creating a new error // here prevents the system from freezing when the network request to // segment fails for any reason. - const safeError = new Error(err.message); - safeError.stack = err.stack; + const safeError = new Error(message); + if (stack) { + safeError.stack = stack; + } return reject(safeError); } return resolve(); }; - this._submitSegmentAPICall('track', payload, callback); + this.#submitSegmentAPICall('track', payload, callback); if (flushImmediately) { - this.segment.flush(); + this.#segment.flush(); } }); } - // Method below submits the request to analytics SDK. - // It will also add event to controller store - // and pass a callback to remove it from store once request is submitted to segment - // Saving segmentApiCalls in controller store in MV3 ensures that events are tracked - // even if service worker terminates before events are submiteed to segment. - _submitSegmentAPICall(eventType, payload, callback) { + /* + * Method below submits the request to analytics SDK. + * It will also add event to controller store + * and pass a callback to remove it from store once request is submitted to segment + * Saving segmentApiCalls in controller store in MV3 ensures that events are tracked + * even if service worker terminates before events are submitted to segment. + */ + #submitSegmentAPICall( + eventType: SegmentEventType, + payload: Partial, + callback?: (result: unknown) => unknown, + ): void { const { metaMetricsId, participateInMetaMetrics, @@ -1129,13 +1364,19 @@ export default class MetaMetricsController { timestamp = payloadDate; } } - const modifiedPayload = { ...payload, messageId, timestamp }; + const modifiedPayload = { + ...payload, + messageId, + timestamp, + }; this.store.updateState({ ...this.store.getState(), latestNonAnonymousEventTimestamp: modifiedPayload.anonymousId === METAMETRICS_ANONYMOUS_ID ? latestNonAnonymousEventTimestamp : timestamp.valueOf(), + // @ts-expect-error The reason this is needed is that the event property in the payload can be missing, + // whereas the state expects it to be present. It's unclear how best to handle this discrepancy. segmentApiCalls: { ...this.store.getState().segmentApiCalls, [messageId]: { @@ -1147,7 +1388,7 @@ export default class MetaMetricsController { }, }, }); - const modifiedCallback = (result) => { + const modifiedCallback = (result: unknown) => { const { segmentApiCalls } = this.store.getState(); delete segmentApiCalls[messageId]; this.store.updateState({ @@ -1155,17 +1396,16 @@ export default class MetaMetricsController { }); return callback?.(result); }; - this.segment[eventType](modifiedPayload, modifiedCallback); + this.#segment[eventType](modifiedPayload, modifiedCallback); } /** * Returns the total number of Ethereum addresses with saved petnames, * including all chain ID variations. * - * @param {object} metamaskState - * @returns {number} + * @param metamaskState */ - _getPetnameAddressCount(metamaskState) { + #getPetnameAddressCount(metamaskState: MetaMaskState): number { const addressNames = metamaskState.names?.[NameType.ETHEREUM_ADDRESS] ?? {}; return Object.keys(addressNames).reduce((totalCount, address) => { diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 7fb87c6d143b..479c4cb0f14d 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -17,13 +17,14 @@ import { NETWORK_TYPES, TEST_NETWORK_TICKER_MAP, } from '../../../shared/constants/network'; -import MMIController from './mmi-controller'; +import { MMIController, AllowedActions } from './mmi-controller'; 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 { InfuraNetworkType } from '@metamask/controller-utils'; import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ @@ -39,6 +40,21 @@ jest.mock('./permissions', () => ({ }), })); +export const createMockNetworkConfiguration = ( + override?: Partial, +): NetworkConfiguration => { + return { + chainId: CHAIN_IDS.SEPOLIA, + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + name: 'Mock Network', + nativeCurrency: 'MOCK TOKEN', + rpcEndpoints: [], + defaultBlockExplorerUrlIndex: 0, + ...override, + }; +}; + const mockAccount = { address: '0x758b8178a9A4B7206d1f648c4a77C515Cbac7001', id: 'mock-id', @@ -73,10 +89,10 @@ describe('MMIController', function () { mmiConfigurationController, controllerMessenger, accountsController, - networkController, keyringController, metaMetricsController, - custodyController; + custodyController, + mmiControllerMessenger; beforeEach(async function () { const mockMessenger = { @@ -89,22 +105,10 @@ describe('MMIController', function () { subscribe: jest.fn(), }; - networkController = new NetworkController({ - messenger: new ControllerMessenger().getRestricted({ - name: 'NetworkController', - allowedEvents: [ - 'NetworkController:stateChange', - 'NetworkController:networkWillChange', - 'NetworkController:networkDidChange', - 'NetworkController:infuraIsBlocked', - 'NetworkController:infuraIsUnblocked', - ], - }), - state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), - infuraProjectId: 'mock-infura-project-id', - }); - - controllerMessenger = new ControllerMessenger(); + const controllerMessenger = new ControllerMessenger< + AllowedActions, + never + >(); accountsController = new AccountsController({ messenger: controllerMessenger.getRestricted({ @@ -212,7 +216,31 @@ describe('MMIController', function () { onNetworkDidChange: jest.fn(), }); - const mmiControllerMessenger = controllerMessenger.getRestricted({ + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue(mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA })), + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + InfuraNetworkType['sepolia'], + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn().mockReturnValue({ + configuration: { + chainId: CHAIN_IDS.SEPOLIA, + } + }), + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkConfigurationByChainId', + jest.fn().mockReturnValue(createMockNetworkConfiguration()), + ); + + mmiControllerMessenger = controllerMessenger.getRestricted({ name: 'MMIController', allowedActions: [ 'AccountsController:getAccountByAddress', @@ -220,6 +248,10 @@ describe('MMIController', function () { 'AccountsController:listAccounts', 'AccountsController:getSelectedAccount', 'AccountsController:setSelectedAccount', + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByChainId' ], }); @@ -237,8 +269,6 @@ describe('MMIController', function () { messenger: mockMessenger, }), isEthSignEnabled: jest.fn(), - getAllState: jest.fn(), - getCurrentChainId: jest.fn(), }), appStateController: new AppStateController({ addUnlockListener: jest.fn(), @@ -255,7 +285,6 @@ describe('MMIController', function () { }) } }), - networkController, permissionController, custodyController, metaMetricsController, @@ -509,9 +538,7 @@ describe('MMIController', function () { CUSTODIAN_TYPES['CUSTODIAN-TYPE'] = { keyringClass: { type: 'mock-keyring-class' }, }; - mmiController.messenger.call = jest - .fn() - .mockReturnValue({ address: '0x1' }); + jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue({ address: '0x1' }); mmiController.custodyController.getCustodyTypeByAddress = jest .fn() .mockReturnValue('custodian-type'); @@ -630,9 +657,7 @@ describe('MMIController', function () { mmiController.custodyController.getAccountDetails = jest .fn() .mockReturnValue({}); - mmiController.messenger.call = jest - .fn() - .mockReturnValue([mockAccount, mockAccount2]); + jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue([mockAccount, mockAccount2]); mmiController.mmiConfigurationController.store.getState = jest .fn() .mockReturnValue({ @@ -703,7 +728,7 @@ describe('MMIController', function () { describe('handleMmiCheckIfTokenIsPresent', () => { it('should check if a token is present', async () => { - mmiController.messenger.call = jest + mmiController.messagingSystem.call = jest .fn() .mockReturnValue({ address: '0x1' }); mmiController.custodyController.getCustodyTypeByAddress = jest @@ -735,7 +760,7 @@ describe('MMIController', function () { describe('handleMmiDashboardData', () => { it('should return internalAccounts as identities', async () => { - const controllerMessengerSpy = jest.spyOn(controllerMessenger, 'call'); + const controllerMessengerSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.handleMmiDashboardData(); expect(controllerMessengerSpy).toHaveBeenCalledWith( @@ -813,7 +838,7 @@ describe('MMIController', function () { describe('setAccountAndNetwork', () => { it('should set a new selected account if the selectedAddress and the address from the arguments is different', async () => { - const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call'); + const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.setAccountAndNetwork( 'mock-origin', mockAccount2.address, @@ -831,14 +856,14 @@ describe('MMIController', function () { }); it('should not set a new selected account the accounts are the same', async () => { - const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call'); + const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.setAccountAndNetwork( 'mock-origin', mockAccount.address, '0x1', ); - expect(selectedAccountSpy).toHaveBeenCalledTimes(1); + expect(selectedAccountSpy).toHaveBeenCalledTimes(4); const selectedAccount = accountsController.getSelectedAccount(); expect(selectedAccount.id).toBe(mockAccount.id); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 65cdac69ba0b..ac8de8b76656 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -1,4 +1,3 @@ -import EventEmitter from 'events'; import log from 'loglevel'; import { captureException } from '@sentry/browser'; import { @@ -21,13 +20,13 @@ import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { TransactionMeta } from '@metamask/transaction-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; +import { NetworkState } from '@metamask/network-controller'; import { 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'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; @@ -41,15 +40,12 @@ import { Label, Signature, ConnectionRequest, + MMIControllerMessenger, } from '../../../shared/constants/mmi-controller'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; import AccountTrackerController from './account-tracker-controller'; import { AppStateController } from './app-state-controller'; -import { PreferencesController } from './preferences-controller'; type UpdateCustodianTransactionsParameters = { keyring: CustodyKeyring; @@ -64,7 +60,7 @@ type UpdateCustodianTransactionsParameters = { setTxHash: (txId: string, txHash: string) => void; }; -export default class MMIController extends EventEmitter { +export class MMIController { public opts: MMIControllerOptions; public mmiConfigurationController: MmiConfigurationController; @@ -73,8 +69,6 @@ export default class MMIController extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any public keyringController: any; - public preferencesController: PreferencesController; - public appStateController: AppStateController; public transactionUpdateController: TransactionUpdateController; @@ -93,7 +87,7 @@ export default class MMIController extends EventEmitter { private metaMetricsController: MetaMetricsController; - private networkController: NetworkController; + #networkControllerState: NetworkState; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -101,9 +95,7 @@ export default class MMIController extends EventEmitter { private signatureController: SignatureController; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private messenger: any; + private messagingSystem: MMIControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -139,13 +131,10 @@ export default class MMIController extends EventEmitter { }; constructor(opts: MMIControllerOptions) { - super(); - this.opts = opts; - this.messenger = opts.messenger; + this.messagingSystem = opts.messenger; this.mmiConfigurationController = opts.mmiConfigurationController; this.keyringController = opts.keyringController; - this.preferencesController = opts.preferencesController; this.appStateController = opts.appStateController; this.transactionUpdateController = opts.transactionUpdateController; this.custodyController = opts.custodyController; @@ -153,7 +142,6 @@ export default class MMIController extends EventEmitter { this.getPendingNonce = opts.getPendingNonce; this.accountTrackerController = opts.accountTrackerController; this.metaMetricsController = opts.metaMetricsController; - this.networkController = opts.networkController; this.permissionController = opts.permissionController; this.signatureController = opts.signatureController; this.platform = opts.platform; @@ -214,6 +202,10 @@ export default class MMIController extends EventEmitter { this.setConnectionRequest(payload); }, ); + + this.#networkControllerState = this.messagingSystem.call( + 'NetworkController:getState', + ); } // End of constructor async persistKeyringsAfterRefreshTokenChange() { @@ -402,7 +394,10 @@ export default class MMIController extends EventEmitter { // Check if any address is already added if ( newAccounts.some((address) => - this.messenger.call('AccountsController:getAccountByAddress', address), + this.messagingSystem.call( + 'AccountsController:getAccountByAddress', + address, + ), ) ) { throw new Error('Cannot import duplicate accounts'); @@ -502,15 +497,17 @@ export default class MMIController extends EventEmitter { // If the label is defined if (label) { // Set the label for the address - const account = this.messenger.call( + const account = this.messagingSystem.call( 'AccountsController:getAccountByAddress', address, ); - this.messenger.call( - 'AccountsController:setAccountName', - account.id, - label, - ); + if (account) { + this.messagingSystem.call( + 'AccountsController:setAccountName', + account.id, + label, + ); + } } } }); @@ -552,7 +549,7 @@ export default class MMIController extends EventEmitter { ) { let currentCustodyType: string = ''; if (!custodianType) { - const { address } = this.messenger.call( + const { address } = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); currentCustodyType = this.custodyController.getCustodyTypeByAddress( @@ -637,7 +634,7 @@ export default class MMIController extends EventEmitter { // Based on a custodian name, get all the tokens associated with that custodian async getCustodianJWTList(custodianEnvName: string) { - const internalAccounts = this.messenger.call( + const internalAccounts = this.messagingSystem.call( 'AccountsController:listAccounts', ); @@ -736,7 +733,8 @@ export default class MMIController extends EventEmitter { const currentAddress = address || - this.messenger.call('AccountsController:getSelectedAccount').address; + this.messagingSystem.call('AccountsController:getSelectedAccount') + .address; const currentCustodyType = this.custodyController.getCustodyTypeByAddress( toChecksumHexAddress(currentAddress), ); @@ -761,7 +759,7 @@ export default class MMIController extends EventEmitter { // TEMP: Convert internal accounts to match identities format // TODO: Convert handleMmiPortfolio to use internal accounts - const internalAccounts = this.messenger + const internalAccounts = this.messagingSystem .call('AccountsController:listAccounts') .map((internalAccount: InternalAccount) => { return { @@ -774,9 +772,10 @@ export default class MMIController extends EventEmitter { this.custodyController.getAccountDetails(address); const extensionId = this.extension.runtime.id; - const networks = Object.values( - this.networkController.state.networkConfigurationsByChainId, + const { networkConfigurationsByChainId } = this.messagingSystem.call( + 'NetworkController:getState', ); + const networks = Object.values(networkConfigurationsByChainId); return handleMmiPortfolio({ keyringAccounts, @@ -848,30 +847,38 @@ export default class MMIController extends EventEmitter { async setAccountAndNetwork(origin: string, address: string, chainId: number) { await this.appStateController.getUnlockPromise(true); const addressToLowerCase = address.toLowerCase(); - const { address: selectedAddress } = this.messenger.call( + const { address: selectedAddress } = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); if (selectedAddress.toLowerCase() !== addressToLowerCase) { - const internalAccount = this.messenger.call( + const internalAccount = this.messagingSystem.call( 'AccountsController:getAccountByAddress', addressToLowerCase, ); - this.messenger.call( - 'AccountsController:setSelectedAccount', - internalAccount.id, - ); + if (internalAccount) { + this.messagingSystem.call( + 'AccountsController:setSelectedAccount', + internalAccount.id, + ); + } } - const selectedChainId = getCurrentChainId({ - metamask: this.networkController.state, - }); + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId: selectedChainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); if (selectedChainId !== toHex(chainId)) { - const networkConfiguration = - this.networkController.state.networkConfigurationsByChainId[ - toHex(chainId) - ]; + const networkConfiguration = this.messagingSystem.call( + 'NetworkController:getNetworkConfigurationByChainId', + toHex(chainId), + ); const { networkClientId } = networkConfiguration?.rpcEndpoints?.[ @@ -879,7 +886,10 @@ export default class MMIController extends EventEmitter { ] ?? {}; if (networkClientId) { - await this.networkController.setActiveNetwork(networkClientId); + await this.messagingSystem.call( + 'NetworkController:setActiveNetwork', + networkClientId, + ); } } diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 9c28ed7c43a0..39a2d49648b2 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -730,9 +730,10 @@ describe('preferences controller', () => { expect(controller.state.preferences).toStrictEqual({ autoLockTimeLimit: undefined, showExtensionInFullSizeView: false, + privacyMode: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: null, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, @@ -749,6 +750,7 @@ describe('preferences controller', () => { order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, }); }); @@ -760,10 +762,11 @@ describe('preferences controller', () => { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: null, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, + privacyMode: false, redesignedConfirmationsEnabled: true, redesignedTransactionsEnabled: true, shouldShowAggregatedBalancePopover: true, @@ -777,6 +780,7 @@ describe('preferences controller', () => { order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, }); }); }); @@ -833,6 +837,23 @@ describe('preferences controller', () => { }); }); + describe('overrideContentSecurityPolicyHeader', () => { + it('defaults overrideContentSecurityPolicyHeader to true', () => { + const { controller } = setupController({}); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(true); + }); + + it('set overrideContentSecurityPolicyHeader to false', () => { + const { controller } = setupController({}); + controller.setOverrideContentSecurityPolicyHeader(false); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(false); + }); + }); + describe('snapsAddSnapAccountModalDismissed', () => { it('defaults snapsAddSnapAccountModalDismissed to false', () => { const { controller } = setupController({}); @@ -849,4 +870,19 @@ describe('preferences controller', () => { ); }); }); + + describe('setSolanaSupportEnabled', () => { + const { controller } = setupController({}); + it('has the default value as false', () => { + expect(controller.state.solanaSupportEnabled).toStrictEqual(false); + }); + + it('sets the solanaSupportEnabled property in state to true and then false', () => { + controller.setSolanaSupportEnabled(true); + expect(controller.state.solanaSupportEnabled).toStrictEqual(true); + + controller.setSolanaSupportEnabled(false); + expect(controller.state.solanaSupportEnabled).toStrictEqual(false); + }); + }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index 536ec33b34eb..dce2ef3d0512 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -103,7 +103,7 @@ export type Preferences = { showExtensionInFullSizeView: boolean; showFiatInTestnets: boolean; showTestNetworks: boolean; - smartTransactionsOptInStatus: boolean | null; + smartTransactionsOptInStatus: boolean; showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; @@ -112,6 +112,7 @@ export type Preferences = { redesignedTransactionsEnabled: boolean; featureNotificationsEnabled: boolean; showMultiRpcModal: boolean; + privacyMode: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; tokenSortConfig: { @@ -119,6 +120,7 @@ export type Preferences = { order: string; sortCallback: string; }; + tokenNetworkFilter: Record; shouldShowAggregatedBalancePopover: boolean; }; @@ -131,6 +133,7 @@ export type PreferencesControllerState = Omit< useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; + overrideContentSecurityPolicyHeader: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; use4ByteResolution: boolean; @@ -139,6 +142,9 @@ export type PreferencesControllerState = Omit< ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(solana) + solanaSupportEnabled: boolean; + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; addSnapAccountEnabled?: boolean; @@ -170,6 +176,7 @@ export const getDefaultPreferencesControllerState = useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useMultiAccountBalanceChecker: true, useSafeChainsListValidation: true, // set to true means the dynamic list from the API is being used @@ -182,6 +189,9 @@ export const getDefaultPreferencesControllerState = openSeaEnabled: true, securityAlertsEnabled: true, watchEthereumAccountEnabled: false, + ///: BEGIN:ONLY_INCLUDE_IF(solana) + solanaSupportEnabled: false, + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: false, bitcoinTestnetSupportEnabled: false, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -203,7 +213,7 @@ export const getDefaultPreferencesControllerState = 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 + smartTransactionsOptInStatus: true, showNativeTokenAsMainBalance: false, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, @@ -214,12 +224,14 @@ export const getDefaultPreferencesControllerState = isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, showMultiRpcModal: false, + privacyMode: false, shouldShowAggregatedBalancePopover: true, // by default user should see popover; tokenSortConfig: { key: 'tokenFiatAmount', order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, }, // ENS decentralized website resolution ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, @@ -296,6 +308,10 @@ const controllerMetadata = { persist: true, anonymous: true, }, + overrideContentSecurityPolicyHeader: { + persist: true, + anonymous: true, + }, useMultiAccountBalanceChecker: { persist: true, anonymous: true, @@ -336,6 +352,10 @@ const controllerMetadata = { persist: true, anonymous: false, }, + solanaSupportEnabled: { + persist: true, + anonymous: false, + }, bitcoinSupportEnabled: { persist: true, anonymous: false, @@ -667,6 +687,20 @@ export class PreferencesController extends BaseController< } ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(solana) + /** + * Setter for the `solanaSupportEnabled` property. + * + * @param solanaSupportEnabled - Whether or not the user wants to + * enable the "Add a new Solana account" button. + */ + setSolanaSupportEnabled(solanaSupportEnabled: boolean): void { + this.update((state) => { + state.solanaSupportEnabled = solanaSupportEnabled; + }); + } + ///: END:ONLY_INCLUDE_IF + /** * Setter for the `bitcoinSupportEnabled` property. * @@ -981,6 +1015,20 @@ export class PreferencesController extends BaseController< }); } + /** + * A setter for the user preference to override the Content-Security-Policy header + * + * @param overrideContentSecurityPolicyHeader - User preference for overriding the Content-Security-Policy header. + */ + setOverrideContentSecurityPolicyHeader( + overrideContentSecurityPolicyHeader: boolean, + ): void { + this.update((state) => { + state.overrideContentSecurityPolicyHeader = + overrideContentSecurityPolicyHeader; + }); + } + /** * A setter for the incomingTransactions in preference to be updated * diff --git a/app/scripts/fixtures/with-preferences.js b/app/scripts/fixtures/with-preferences.js index 8d1e4293e8a4..c3a482ef8f94 100644 --- a/app/scripts/fixtures/with-preferences.js +++ b/app/scripts/fixtures/with-preferences.js @@ -13,6 +13,7 @@ export const FIXTURES_PREFERENCES = { showNftAutodetectModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + privacyMode: false, }, featureFlags: { sendHexData: true, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 7a322148c847..826aa04018d9 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -150,6 +150,7 @@ const jsonData = JSON.stringify({ useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useTokenDetection: false, useCollectibleDetection: false, openSeaEnabled: false, @@ -165,7 +166,7 @@ const jsonData = JSON.stringify({ showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: true, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, showMultiRpcModal: false, }, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index a1c5a036f13f..cb57c681649f 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -53,6 +53,8 @@ const RATE_LIMIT_MAP = { [MESSAGE_TYPE.ETH_DECRYPT]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, + [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, + [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: RATE_LIMIT_TYPES.TIMEOUT, [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: RATE_LIMIT_TYPES.TIMEOUT, [MESSAGE_TYPE.SEND_METADATA]: RATE_LIMIT_TYPES.BLOCKED, @@ -126,6 +128,8 @@ const EVENT_NAME_MAP = { */ const TRANSFORM_PARAMS_MAP = { [MESSAGE_TYPE.WATCH_ASSET]: ({ type }) => ({ type }), + [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN]: ([{ chainId }]) => ({ chainId }), + [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN]: ([{ chainId }]) => ({ chainId }), }; const rateLimitTimeoutsByMethod = {}; diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 01daaf2974a4..244a995bf5f7 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -883,6 +883,34 @@ describe('createRPCMethodTrackingMiddleware', () => { }, { type: 'ERC20' }, ], + [ + 'only the chain ID', + 'wallet_addEthereumChain', + [ + { + chainId: '0x64', + chainName: 'Gnosis', + rpcUrls: ['https://rpc.gnosischain.com'], + iconUrls: [ + 'https://xdaichain.com/fake/example/url/xdai.svg', + 'https://xdaichain.com/fake/example/url/xdai.png', + ], + nativeCurrency: { + name: 'XDAI', + symbol: 'XDAI', + decimals: 18, + }, + blockExplorerUrls: ['https://blockscout.com/poa/xdai/'], + }, + ], + { chainId: '0x64' }, + ], + [ + 'only the chain ID', + 'wallet_switchEthereumChain', + [{ chainId: '0x123' }], + { chainId: '0x123' }, + ], ])( `should include %s in the '%s' tracked events params property`, async (_, method, params, expected) => { diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 7eb8dc0cc5a2..ebfdbe3f04d7 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -86,12 +86,11 @@ export function createPPOMMiddleware< return; } - const isSupportedChain = await isChainSupported(chainId); - if ( !securityAlertsEnabled || !CONFIRMATION_METHODS.includes(req.method) || - !isSupportedChain + // Do not move this call above this check because it will result in unnecessary calls + !(await isChainSupported(chainId)) ) { return; } diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 7662c364b651..e8c54ee5eb53 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -11,7 +11,7 @@ import { SignatureController } from '@metamask/signature-controller'; import { BlockaidReason, BlockaidResultType, - SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, + SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST, SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; @@ -123,7 +123,7 @@ export function handlePPOMError( } export async function isChainSupported(chainId: Hex): Promise { - let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS; + let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST; try { if (isSecurityAlertsAPIEnabled()) { diff --git a/app/scripts/lib/ppom/security-alerts-api.test.ts b/app/scripts/lib/ppom/security-alerts-api.test.ts index 9d2d97652d4f..460139c1d359 100644 --- a/app/scripts/lib/ppom/security-alerts-api.test.ts +++ b/app/scripts/lib/ppom/security-alerts-api.test.ts @@ -27,6 +27,8 @@ const RESPONSE_MOCK = { description: 'Test Description', }; +const BASE_URL = 'https://example.com'; + describe('Security Alerts API', () => { const fetchMock = jest.fn(); @@ -40,7 +42,7 @@ describe('Security Alerts API', () => { json: async () => RESPONSE_MOCK, }); - process.env.SECURITY_ALERTS_API_URL = 'https://example.com'; + process.env.SECURITY_ALERTS_API_URL = BASE_URL; }); describe('validateWithSecurityAlertsAPI', () => { @@ -54,8 +56,14 @@ describe('Security Alerts API', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith( - `https://example.com/validate/${CHAIN_ID_MOCK}`, - expect.any(Object), + `${BASE_URL}/validate/${CHAIN_ID_MOCK}`, + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(REQUEST_MOCK), + headers: { + 'Content-Type': 'application/json', + }, + }), ); }); @@ -101,7 +109,7 @@ describe('Security Alerts API', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith( - `https://example.com/supportedChains`, + `${BASE_URL}/supportedChains`, undefined, ); }); diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 1b9e9f4ddbfc..354bb0bbb620 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -424,12 +424,17 @@ export function rewriteReport(report) { if (!report.extra) { report.extra = {}; } - - report.extra.appState = appState; - if (browser.runtime && browser.runtime.id) { - report.extra.extensionId = browser.runtime.id; + if (!report.tags) { + report.tags = {}; } - report.extra.installType = installType; + + Object.assign(report.extra, { + appState, + installType, + extensionId: browser.runtime?.id, + }); + + report.tags.installType = installType; } catch (err) { log('Error rewriting report', err); } diff --git a/app/scripts/lib/transaction/metrics.test.ts b/app/scripts/lib/transaction/metrics.test.ts index 75ea5c4b84c0..7dcedd4e467e 100644 --- a/app/scripts/lib/transaction/metrics.test.ts +++ b/app/scripts/lib/transaction/metrics.test.ts @@ -17,6 +17,7 @@ import { import { MetaMetricsTransactionEventSource, MetaMetricsEventCategory, + MetaMetricsEventUiCustomization, } from '../../../../shared/constants/metametrics'; import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils'; import { @@ -147,6 +148,7 @@ describe('Transaction metrics', () => { eip_1559_version: '0', gas_edit_attempted: 'none', gas_estimation_failed: false, + is_smart_transaction: undefined, gas_edit_type: 'none', network: mockNetworkId, referrer: ORIGIN_METAMASK, @@ -155,8 +157,9 @@ describe('Transaction metrics', () => { token_standard: TokenStandard.none, transaction_speed_up: false, transaction_type: TransactionType.simpleSend, - ui_customizations: null, - transaction_advanced_view: null, + ui_customizations: ['redesigned_confirmation'], + transaction_advanced_view: undefined, + transaction_contract_method: undefined, }; expectedSensitiveProperties = { @@ -165,7 +168,7 @@ describe('Transaction metrics', () => { first_seen: 1624408066355, gas_limit: '0x7b0d', gas_price: '2', - transaction_contract_method: undefined, + transaction_contract_address: undefined, transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY, transaction_replaced: undefined, }; @@ -233,7 +236,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['gas_estimation_failed'], + ui_customizations: [ + 'gas_estimation_failed', + 'redesigned_confirmation', + ], gas_estimation_failed: true, }, sensitiveProperties: expectedSensitiveProperties, @@ -263,7 +269,10 @@ describe('Transaction metrics', () => { ...expectedProperties, security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], ppom_eth_call_count: 5, ppom_eth_getCode_count: 3, }, @@ -353,7 +362,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -370,7 +382,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -490,7 +505,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -510,7 +528,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -687,7 +708,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -709,7 +733,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -731,6 +758,72 @@ describe('Transaction metrics', () => { mockTransactionMetricsRequest.finalizeEventFragment, ).toBeCalledWith(expectedUniqueId); }); + + it('should create, update, finalize event fragment with transaction_contract_address', async () => { + mockTransactionMeta.txReceipt = { + gasUsed: '0x123', + status: '0x0', + }; + mockTransactionMeta.submittedTime = 123; + mockTransactionMeta.status = TransactionStatus.confirmed; + mockTransactionMeta.type = TransactionType.contractInteraction; + const expectedUniqueId = 'transaction-submitted-1'; + const properties = { + ...expectedProperties, + status: TransactionStatus.confirmed, + transaction_type: TransactionType.contractInteraction, + asset_type: AssetType.unknown, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + is_smart_transaction: undefined, + transaction_advanced_view: undefined, + }; + const sensitiveProperties = { + ...expectedSensitiveProperties, + transaction_contract_address: + '0x1678a085c290ebd122dc42cba69373b5953b831d', + completion_time: expect.any(String), + gas_used: '0.000000291', + status: METRICS_STATUS_FAILED, + }; + + await handleTransactionConfirmed(mockTransactionMetricsRequest, { + ...mockTransactionMeta, + actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + expect(mockTransactionMetricsRequest.createEventFragment).toBeCalledTimes( + 1, + ); + expect(mockTransactionMetricsRequest.createEventFragment).toBeCalledWith({ + actionId: mockActionId, + category: MetaMetricsEventCategory.Transactions, + successEvent: TransactionMetaMetricsEvent.finalized, + uniqueIdentifier: expectedUniqueId, + persist: true, + properties, + sensitiveProperties, + }); + expect(mockTransactionMetricsRequest.updateEventFragment).toBeCalledTimes( + 1, + ); + expect(mockTransactionMetricsRequest.updateEventFragment).toBeCalledWith( + expectedUniqueId, + { + properties, + sensitiveProperties, + }, + ); + expect( + mockTransactionMetricsRequest.finalizeEventFragment, + ).toHaveBeenCalledTimes(1); + expect( + mockTransactionMetricsRequest.finalizeEventFragment, + ).toHaveBeenCalledWith(expectedUniqueId); + }); }); describe('handleTransactionDropped', () => { @@ -820,7 +913,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -841,7 +937,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -947,7 +1046,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -964,7 +1066,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, diff --git a/app/scripts/lib/transaction/metrics.ts b/app/scripts/lib/transaction/metrics.ts index e0be105b1f10..1e0e26dc1c20 100644 --- a/app/scripts/lib/transaction/metrics.ts +++ b/app/scripts/lib/transaction/metrics.ts @@ -56,7 +56,7 @@ import { export type TransactionMetricsRequest = { createEventFragment: ( - options: MetaMetricsEventFragment, + options: Omit, ) => MetaMetricsEventFragment; finalizeEventFragment: ( fragmentId: string, @@ -528,7 +528,12 @@ function createTransactionEventFragment({ transactionMetricsRequest.getEventFragmentById, eventName, transactionMeta, - ) + ) && + /** + * HACK: "transaction-submitted-" fragment hack + * can continue to createEventFragment if "transaction-submitted-" submitted fragment exists + */ + eventName !== TransactionMetaMetricsEvent.submitted ) { return; } @@ -642,25 +647,14 @@ function updateTransactionEventFragment({ switch (eventName) { case TransactionMetaMetricsEvent.approved: - transactionMetricsRequest.updateEventFragment(uniqueId, { - properties: payload.properties, - sensitiveProperties: payload.sensitiveProperties, - }); - break; - case TransactionMetaMetricsEvent.rejected: - transactionMetricsRequest.updateEventFragment(uniqueId, { - properties: payload.properties, - sensitiveProperties: payload.sensitiveProperties, - }); - break; - case TransactionMetaMetricsEvent.finalized: transactionMetricsRequest.updateEventFragment(uniqueId, { properties: payload.properties, sensitiveProperties: payload.sensitiveProperties, }); break; + default: break; } @@ -679,6 +673,7 @@ function finalizeTransactionEventFragment({ switch (eventName) { case TransactionMetaMetricsEvent.approved: + case TransactionMetaMetricsEvent.finalized: transactionMetricsRequest.finalizeEventFragment(uniqueId); break; @@ -688,9 +683,6 @@ function finalizeTransactionEventFragment({ }); break; - case TransactionMetaMetricsEvent.finalized: - transactionMetricsRequest.finalizeEventFragment(uniqueId); - break; default: break; } @@ -916,6 +908,7 @@ async function buildEventFragmentProperties({ let transactionContractMethod; let transactionApprovalAmountVsProposedRatio; let transactionApprovalAmountVsBalanceRatio; + let transactionContractAddress; let transactionType = TransactionType.simpleSend; if (type === TransactionType.swapAndSend) { transactionType = TransactionType.swapAndSend; @@ -928,6 +921,7 @@ async function buildEventFragmentProperties({ } else if (contractInteractionTypes) { transactionType = TransactionType.contractInteraction; transactionContractMethod = contractMethodName; + transactionContractAddress = transactionMeta.txParams?.to; if ( transactionContractMethod === contractMethodNames.APPROVE && tokenStandard === TokenStandard.ERC20 @@ -1060,6 +1054,7 @@ async function buildEventFragmentProperties({ // ui_customizations must come after ...blockaidProperties ui_customizations: uiCustomizations.length > 0 ? uiCustomizations : null, transaction_advanced_view: isAdvancedDetailsOpen, + transaction_contract_method: transactionContractMethod, ...smartTransactionMetricsProperties, ...swapAndSendMetricsProperties, // TODO: Replace `any` with type @@ -1086,8 +1081,8 @@ async function buildEventFragmentProperties({ : TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY, first_seen: time, gas_limit: gasLimit, - transaction_contract_method: transactionContractMethod, transaction_replaced: transactionReplaced, + transaction_contract_address: transactionContractAddress, ...extraParams, ...gasParamsInGwei, // TODO: Replace `any` with type diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4284a2614a9d..d43c12ff24a1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -14,9 +14,9 @@ import { fetchMultiExchangeRate, } from '@metamask/assets-controllers'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { createEngineStream } from '@metamask/json-rpc-middleware-stream'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; -import { createEngineStream } from 'json-rpc-middleware-stream'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; import { debounce, throttle, memoize, wrap } from 'lodash'; import { @@ -283,7 +283,7 @@ import { checkForMultipleVersionsRunning, } from './detect-multiple-instances'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import MMIController from './controllers/mmi-controller'; +import { MMIController } from './controllers/mmi-controller'; import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory'; ///: END:ONLY_INCLUDE_IF import ComposableObservableStore from './lib/ComposableObservableStore'; @@ -765,31 +765,25 @@ export default class MetamaskController extends EventEmitter { }); this.metaMetricsController = new MetaMetricsController({ + initState: initState.MetaMetricsController, segment, - onPreferencesStateChange: preferencesMessenger.subscribe.bind( - preferencesMessenger, - 'PreferencesController:stateChange', - ), preferencesControllerState: { currentLocale: this.preferencesController.state.currentLocale, selectedAddress: this.preferencesController.state.selectedAddress, }, + onPreferencesStateChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', + ), onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:networkDidChange', ), - getNetworkIdentifier: () => { - const { type, rpcUrl } = getProviderConfig({ - metamask: this.networkController.state, - }); - return type === NETWORK_TYPES.RPC ? rpcUrl : type; - }, getCurrentChainId: () => getCurrentChainId({ metamask: this.networkController.state }), version: process.env.METAMASK_VERSION, environment: process.env.METAMASK_ENVIRONMENT, extension: this.extension, - initState: initState.MetaMetricsController, captureException, }); @@ -884,13 +878,13 @@ export default class MetamaskController extends EventEmitter { messenger: currencyRateMessenger, state: initState.CurrencyController, }); - const initialFetchExchangeRate = - this.currencyRateController.fetchExchangeRate.bind( + const initialFetchMultiExchangeRate = + this.currencyRateController.fetchMultiExchangeRate.bind( this.currencyRateController, ); - this.currencyRateController.fetchExchangeRate = (...args) => { + this.currencyRateController.fetchMultiExchangeRate = (...args) => { if (this.preferencesController.state.useCurrencyRateCheck) { - return initialFetchExchangeRate(...args); + return initialFetchMultiExchangeRate(...args); } return { conversionRate: null, @@ -1014,6 +1008,7 @@ export default class MetamaskController extends EventEmitter { state: initState.TokenRatesController, messenger: tokenRatesMessenger, tokenPricesService: new CodefiTokenPricesServiceV2(), + disabled: !this.preferencesController.state.useCurrencyRateCheck, }); this.controllerMessenger.subscribe( @@ -1022,9 +1017,9 @@ export default class MetamaskController extends EventEmitter { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; if (currUseCurrencyRateCheck && !prevUseCurrencyRateCheck) { - this.tokenRatesController.start(); + this.tokenRatesController.enable(); } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { - this.tokenRatesController.stop(); + this.tokenRatesController.disable(); } }, this.preferencesController.state), ); @@ -1380,6 +1375,8 @@ export default class MetamaskController extends EventEmitter { const allowLocalSnaps = process.env.ALLOW_LOCAL_SNAPS; const requireAllowlist = process.env.REQUIRE_SNAPS_ALLOWLIST; + const rejectInvalidPlatformVersion = + process.env.REJECT_INVALID_SNAPS_PLATFORM_VERSION; this.snapController = new SnapController({ environmentEndowmentPermissions: Object.values(EndowmentPermissions), @@ -1394,6 +1391,7 @@ export default class MetamaskController extends EventEmitter { dappsCanUpdateSnaps: true, allowLocalSnaps, requireAllowlist, + rejectInvalidPlatformVersion, }, encryptor: encryptorFactory(600_000), getMnemonic: this.getPrimaryKeyringMnemonic.bind(this), @@ -1788,6 +1786,8 @@ export default class MetamaskController extends EventEmitter { trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), + useAccountsAPI: true, + platform: 'extension', }); const addressBookControllerMessenger = @@ -1804,7 +1804,7 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ state: initState.AlertController, - controllerMessenger: this.controllerMessenger.getRestricted({ + messenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], allowedActions: ['AccountsController:getSelectedAccount'], @@ -1996,11 +1996,9 @@ export default class MetamaskController extends EventEmitter { `${this.keyringController.name}:signPersonalMessage`, `${this.keyringController.name}:signTypedMessage`, `${this.loggingController.name}:add`, + `${this.networkController.name}:getNetworkClientById`, ], }), - getAllState: this.getState.bind(this), - getCurrentChainId: () => - getCurrentChainId({ metamask: this.networkController.state }), trace, }); @@ -2029,6 +2027,8 @@ export default class MetamaskController extends EventEmitter { 'AccountsController:listAccounts', 'AccountsController:getSelectedAccount', 'AccountsController:setSelectedAccount', + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', ], }); @@ -2036,7 +2036,6 @@ export default class MetamaskController extends EventEmitter { messenger: mmiControllerMessenger, mmiConfigurationController: this.mmiConfigurationController, keyringController: this.keyringController, - preferencesController: this.preferencesController, appStateController: this.appStateController, transactionUpdateController: this.transactionUpdateController, custodyController: this.custodyController, @@ -2044,7 +2043,6 @@ export default class MetamaskController extends EventEmitter { getPendingNonce: this.getPendingNonce.bind(this), accountTrackerController: this.accountTrackerController, metaMetricsController: this.metaMetricsController, - networkController: this.networkController, permissionController: this.permissionController, signatureController: this.signatureController, platform: this.platform, @@ -2128,7 +2126,7 @@ export default class MetamaskController extends EventEmitter { const bridgeControllerMessenger = this.controllerMessenger.getRestricted({ name: BRIDGE_CONTROLLER_NAME, - allowedActions: [], + allowedActions: ['AccountsController:getSelectedAccount'], allowedEvents: [], }); this.bridgeController = new BridgeController({ @@ -2392,7 +2390,7 @@ export default class MetamaskController extends EventEmitter { AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, NetworkController: this.networkController, - AlertController: this.alertController.store, + AlertController: this.alertController, OnboardingController: this.onboardingController, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController, @@ -2447,7 +2445,7 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, - AlertController: this.alertController.store, + AlertController: this.alertController, OnboardingController: this.onboardingController, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController, @@ -2599,12 +2597,6 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.state; - const { useCurrencyRateCheck } = preferencesControllerState; - - if (useCurrencyRateCheck) { - this.tokenRatesController.start(); - } - if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.start(); } @@ -2617,12 +2609,6 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.state; - const { useCurrencyRateCheck } = preferencesControllerState; - - if (useCurrencyRateCheck) { - this.tokenRatesController.stop(); - } - if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.stop(); } @@ -3259,6 +3245,7 @@ export default class MetamaskController extends EventEmitter { backup, approvalController, phishingController, + tokenRatesController, // Notification Controllers authenticationController, userStorageController, @@ -3335,6 +3322,12 @@ export default class MetamaskController extends EventEmitter { preferencesController, ), ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(solana) + setSolanaSupportEnabled: + preferencesController.setSolanaSupportEnabled.bind( + preferencesController, + ), + ///: END:ONLY_INCLUDE_IF setBitcoinSupportEnabled: preferencesController.setBitcoinSupportEnabled.bind( preferencesController, @@ -3494,6 +3487,10 @@ export default class MetamaskController extends EventEmitter { preferencesController.setDismissSeedBackUpReminder.bind( preferencesController, ), + setOverrideContentSecurityPolicyHeader: + preferencesController.setOverrideContentSecurityPolicyHeader.bind( + preferencesController, + ), setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind( preferencesController, ), @@ -3952,6 +3949,11 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_DEST_NETWORK}`, ), + [BridgeUserAction.UPDATE_QUOTE_PARAMS]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.UPDATE_QUOTE_PARAMS}`, + ), // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( @@ -4012,15 +4014,21 @@ export default class MetamaskController extends EventEmitter { ), // CurrencyRateController - currencyRateStartPollingByNetworkClientId: - currencyRateController.startPollingByNetworkClientId.bind( - currencyRateController, - ), + currencyRateStartPolling: currencyRateController.startPolling.bind( + currencyRateController, + ), currencyRateStopPollingByPollingToken: currencyRateController.stopPollingByPollingToken.bind( currencyRateController, ), + tokenRatesStartPolling: + tokenRatesController.startPolling.bind(tokenRatesController), + tokenRatesStopPollingByPollingToken: + tokenRatesController.stopPollingByPollingToken.bind( + tokenRatesController, + ), + // GasFeeController gasFeeStartPollingByNetworkClientId: gasFeeController.startPollingByNetworkClientId.bind(gasFeeController), @@ -6646,12 +6654,13 @@ export default class MetamaskController extends EventEmitter { /** * A method that is called by the background when all instances of metamask are closed. - * Currently used to stop polling in the gasFeeController. + * Currently used to stop controller polling. */ onClientClosed() { try { this.gasFeeController.stopAllPolling(); this.currencyRateController.stopAllPolling(); + this.tokenRatesController.stopAllPolling(); this.appStateController.clearPollingTokens(); } catch (error) { console.error(error); @@ -6685,12 +6694,34 @@ export default class MetamaskController extends EventEmitter { * @param {string} origin - the domain to safelist */ safelistPhishingDomain(origin) { + this.metaMetricsController.trackEvent({ + category: MetaMetricsEventCategory.Phishing, + event: MetaMetricsEventName.ProceedAnywayClicked, + properties: { + url: origin, + referrer: { + url: origin, + }, + }, + }); + return this.phishingController.bypass(origin); } async backToSafetyPhishingWarning() { - const extensionURL = this.platform.getExtensionURL(); - await this.platform.switchToAnotherURL(undefined, extensionURL); + const portfolioBaseURL = process.env.PORTFOLIO_URL; + const portfolioURL = `${portfolioBaseURL}/?metamaskEntry=phishing_page_portfolio_button`; + + this.metaMetricsController.trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.PortfolioLinkClicked, + properties: { + location: 'phishing_page', + text: 'Back to safety', + }, + }); + + await this.platform.switchToAnotherURL(undefined, portfolioURL); } /** diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts index c725a2cbd837..b596468853b2 100644 --- a/app/scripts/snaps/preinstalled-snaps.ts +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -6,6 +6,9 @@ import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/preinstalled-snap.json'; ///: END:ONLY_INCLUDE_IF +///: BEGIN:ONLY_INCLUDE_IF(solana) +import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; +///: END:ONLY_INCLUDE_IF // The casts here are less than ideal but we expect the SnapController to validate the inputs. const PREINSTALLED_SNAPS = Object.freeze([ @@ -16,6 +19,9 @@ const PREINSTALLED_SNAPS = Object.freeze([ BitcoinWalletSnap as unknown as PreinstalledSnap, PreinstalledExampleSnap as unknown as PreinstalledSnap, ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(solana) + SolanaWalletSnap as unknown as PreinstalledSnap, + ///: END:ONLY_INCLUDE_IF ]); export default PREINSTALLED_SNAPS; diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 4f06fb8998c4..794037e12761 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -104,7 +104,7 @@ async function start() { if (isManifestV3 && isUIInitialised) { // Currently when service worker is revived we create new streams // in later version we might try to improve it by reviving same streams. - updateUiStreams(); + updateUiStreams(connectionStream); } else { await initializeUiWithTab( activeTab, diff --git a/builds.yml b/builds.yml index bcd035b56bc1..2442ad51475a 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,8 @@ 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.9.1/index.html + - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +47,8 @@ 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.9.1/index.html + - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/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 +69,8 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html + - REJECT_INVALID_SNAPS_PLATFORM_VERSION: false + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/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 +93,8 @@ 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.9.1/index.html + - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.2/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 @@ -131,6 +135,9 @@ features: keyring-snaps: assets: - ./{app,shared,ui}/**/keyring-snaps/** + solana: + assets: + - ./{app,shared,ui}/**/solana/** # Env variables that are required for all types of builds # @@ -173,6 +180,12 @@ env: - SENTRY_MMI_DSN: '' + ### + # Storybook + ### + - STORYBOOK: false + - INFURA_STORYBOOK_PROJECT_ID + ### # Notifications Feature ### @@ -267,12 +280,14 @@ env: - BARAD_DUR: '' # Determines if feature flagged Chain permissions - CHAIN_PERMISSIONS: '' + # Determines if feature flagged Filter toggle + - FILTER_TOKENS_TOGGLE: '' # Enables use of test gas fee flow to debug gas fee estimation - TEST_GAS_FEE_FLOWS: false # Temporary mechanism to enable security alerts API prior to release - - SECURITY_ALERTS_API_ENABLED: '' + - SECURITY_ALERTS_API_ENABLED: 'true' # URL of security alerts API used to validate dApp requests - - SECURITY_ALERTS_API_URL: 'http://localhost:3000' + - SECURITY_ALERTS_API_URL: 'https://security-alerts.api.cx.metamask.io' # API key to authenticate Etherscan requests to avoid rate limiting - ETHERSCAN_API_KEY: '' diff --git a/development/README.md b/development/README.md index 33ab036975e4..86733ef172e6 100644 --- a/development/README.md +++ b/development/README.md @@ -65,7 +65,7 @@ or `https://api.segment.io/v1/batch` respectively. 2. To display Sentry logs, include `DEBUG=metamask:sentry:*` in `.metamaskrc`. -3. To display more verbose logs if not in a developer build, include `METAMASK_DEBUG=true` in `.metamaskrc`. +3. To display more verbose logs if not in a developer build, include `METAMASK_DEBUG=1` in `.metamaskrc`. 4. Ensure metrics are enabled during onboarding or via `Settings > Security & privacy > Participate in MetaMetrics`. diff --git a/development/build/scripts.js b/development/build/scripts.js index 121e000a5673..3b571862df89 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -935,6 +935,7 @@ function setupBundlerDefaults( './**/node_modules/firebase', './**/node_modules/@firebase', './**/node_modules/marked', + './**/node_modules/@solana', ], global: true, }, diff --git a/development/build/utils.js b/development/build/utils.js index 525815d2520a..626aacd588c7 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa((globalThis.browser||chrome).runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/create-static-server.js b/development/create-static-server.js index a8d5e28b0088..8e55fa54ca13 100755 --- a/development/create-static-server.js +++ b/development/create-static-server.js @@ -4,10 +4,17 @@ const path = require('path'); const serveHandler = require('serve-handler'); -const createStaticServer = (rootDirectory) => { +/** + * Creates an HTTP server that serves static files from a directory using serve-handler. + * If a request URL starts with `/node_modules/`, it rewrites the URL and serves files from the `node_modules` directory. + * + * @param { NonNullable[2]> } options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler + * @returns {http.Server} An instance of an HTTP server configured with the specified options. + */ +const createStaticServer = (options) => { return http.createServer((request, response) => { if (request.url.startsWith('/node_modules/')) { - request.url = request.url.substr(14); + request.url = request.url.slice(14); return serveHandler(request, response, { directoryListing: false, public: path.resolve('./node_modules'), @@ -15,7 +22,7 @@ const createStaticServer = (rootDirectory) => { } return serveHandler(request, response, { directoryListing: false, - public: rootDirectory, + ...options, }); }); }; diff --git a/development/generate-attributions/package.json b/development/generate-attributions/package.json index 92bf1a5153c2..5778cbd0c634 100644 --- a/development/generate-attributions/package.json +++ b/development/generate-attributions/package.json @@ -9,7 +9,7 @@ }, "engines": { "node": ">= 20", - "yarn": "^4.4.1" + "yarn": "^4.5.1" }, "lavamoat": { "allowScripts": { diff --git a/development/static-server.js b/development/static-server.js index bb15133d6fdd..ec3a51a512f0 100755 --- a/development/static-server.js +++ b/development/static-server.js @@ -31,7 +31,7 @@ const onRequest = (request, response) => { }; const startServer = ({ port, rootDirectory }) => { - const server = createStaticServer(rootDirectory); + const server = createStaticServer({ public: rootDirectory }); server.on('request', onRequest); diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts index 3a3ef729eacf..b6390654a4bb 100644 --- a/development/webpack/test/plugins.SelfInjectPlugin.test.ts +++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts @@ -55,7 +55,7 @@ describe('SelfInjectPlugin', () => { // reference the `sourceMappingURL` assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } else { // the new source should NOT reference the new sourcemap, since it's @@ -66,7 +66,7 @@ describe('SelfInjectPlugin', () => { // console. assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index b80f6102ab75..18d3624310ae 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -6,6 +6,19 @@ import type { SelfInjectPluginOptions, Source, Compiler } from './types'; export { type SelfInjectPluginOptions } from './types'; +/** + * Generates a runtime URL expression for a given path. + * + * This function constructs a URL string using the `runtime.getURL` method + * from either the `globalThis.browser` or `chrome` object, depending on + * which one is available in the global scope. + * + * @param path - The path of the runtime URL. + * @returns The constructed runtime URL string. + */ +const getRuntimeURLExpression = (path: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; + /** * Default options for the SelfInjectPlugin. */ @@ -13,8 +26,11 @@ const defaultOptions = { // The default `sourceUrlExpression` is configured for browser extensions. // It generates the absolute url of the given file as an extension url. // e.g., `chrome-extension:///scripts/inpage.js` - sourceUrlExpression: (filename: string) => - `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(filename)})`, + sourceUrlExpression: getRuntimeURLExpression, + // The default `nonceExpression` is configured for browser extensions. + // It generates the absolute url of a path as an extension url in base64. + // e.g., `Y2hyb21lLWV4dGVuc2lvbjovLzxleHRlbnNpb24taWQ+Lw==` + nonceExpression: (path: string) => `btoa(${getRuntimeURLExpression(path)})`, } satisfies SelfInjectPluginOptions; /** @@ -142,6 +158,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); + newSource.add(`s.nonce=${this.options.nonceExpression('/')};`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts index 2240227bd76a..e70467cd270b 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts @@ -23,7 +23,7 @@ export type SelfInjectPluginOptions = { * will be injected into matched file to provide a sourceURL for the self * injected script. * - * Defaults to `(filename: string) => (globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * Defaults to `(filename: string) => (globalThis.browser||chrome).runtime.getURL("${filename}")` * * @example Custom * ```js @@ -39,11 +39,22 @@ export type SelfInjectPluginOptions = { * * ```js * { - * sourceUrlExpression: (filename) => `(globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * sourceUrlExpression: (filename) => `(globalThis.browser||chrome).runtime.getURL("${filename}")` * } * ``` * @param filename - the chunk's relative filename as it will exist in the output directory * @returns */ sourceUrlExpression?: (filename: string) => string; + /** + * A function that returns a JavaScript expression escaped as a string which + * will be injected into matched file to set a nonce for the self + * injected script. + * + * Defaults to `(path: string) => btoa((globalThis.browser||chrome).runtime.getURL("${path}"))` + * + * @param path - the path to be encoded as a nonce + * @returns + */ + nonceExpression?: (path: string) => string; }; diff --git a/development/webpack/webpack.integration.tests.config.ts b/development/webpack/webpack.integration.tests.config.ts new file mode 100644 index 000000000000..77e032581180 --- /dev/null +++ b/development/webpack/webpack.integration.tests.config.ts @@ -0,0 +1,116 @@ +/** + * @file The webpack configuration file to enable debug previewing for UI integration tests. + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { + type Configuration, + type WebpackPluginInstance, + ProgressPlugin, +} from 'webpack'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import CopyPlugin from 'copy-webpack-plugin'; +import rtlCss from 'postcss-rtlcss'; +import autoprefixer from 'autoprefixer'; + +const context = join(__dirname, '../../app'); +const browsersListPath = join(context, '../.browserslistrc'); +const browsersListQuery = readFileSync(browsersListPath, 'utf8'); + +const plugins: WebpackPluginInstance[] = [ + new CopyPlugin({ + patterns: [ + { from: join(context, '_locales'), to: '_locales' }, // translations + // misc images + // TODO: fix overlap between this folder and automatically bundled assets + { from: join(context, 'images'), to: 'images' }, + ], + }), + new ProgressPlugin(), + new MiniCssExtractPlugin({ filename: '[name].css' }), +]; + +const config = { + entry: { + index: join(context, '../ui/css/index.scss'), + }, + plugins, + mode: 'development', + context, + stats: 'normal', + name: `MetaMask UI integration test`, + output: { + path: join(context, '..', 'test/integration/config/assets'), + clean: true, + }, + // note: loaders in a `use` array are applied in *reverse* order, i.e., bottom + // to top, (or right to left depending on the current formatting of the file) + module: { + rules: [ + // css, sass/scss + { + test: /\.(css|sass|scss)$/u, + use: [ + MiniCssExtractPlugin.loader, + // Resolves CSS `@import` and `url()` paths and loads the files. + { + loader: 'css-loader', + options: { + url: true, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + autoprefixer({ overrideBrowserslist: browsersListQuery }), + rtlCss({ processEnv: false }), + ], + }, + }, + }, + { + loader: 'resolve-url-loader', + }, + // Compiles Sass to CSS + { + loader: 'sass-loader', + options: { + // Use 'sass-embedded', as it is usually faster than 'sass' + implementation: 'sass-embedded', + sassOptions: { + api: 'modern', + // We don't need to specify the charset because the HTML + // already does and browsers use the HTML's charset for CSS. + // Additionally, webpack + sass can cause problems with the + // charset placement, as described here: + // https://github.com/webpack-contrib/css-loader/issues/1212 + charset: false, + // The order of includePaths is important; prefer our own + // folders over `node_modules` + includePaths: [ + // enables aliases to `@use design - system`, + // `@use utilities`, etc. + join(context, '../ui/css'), + join(context, '../node_modules'), + ], + // Disable the webpackImporter, as we: + // a) don't want to rely on it in case we want to switch away + // from webpack in the future + // b) the sass importer is faster + // c) the "modern" sass api doesn't work with the + // webpackImporter yet. + webpackImporter: false, + }, + sourceMap: true, + }, + }, + ], + }, + ], + }, +} as const satisfies Configuration; + +export default config; diff --git a/jest.config.js b/jest.config.js index f1d38ab4aea3..56a75bfc68ed 100644 --- a/jest.config.js +++ b/jest.config.js @@ -35,6 +35,7 @@ module.exports = { '/development/**/*.test.(js|ts|tsx)', '/test/unit-global/**/*.test.(js|ts|tsx)', '/test/e2e/helpers.test.js', + '/test/e2e/helpers/**/*.test.(js|ts|tsx)', ], testPathIgnorePatterns: ['/development/webpack/'], testTimeout: 5500, diff --git a/jest.integration.config.js b/jest.integration.config.js index 6f5d79484386..d7236b832aed 100644 --- a/jest.integration.config.js +++ b/jest.integration.config.js @@ -35,4 +35,13 @@ module.exports = { customExportConditions: ['node', 'node-addons'], }, workerIdleMemoryLimit: '500MB', + transform: { + // Use babel-jest to transpile tests with the next/babel preset + // https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object + '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', + '^.+\\.(css|scss|sass|less)$': 'jest-preview/transforms/css', + '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': + 'jest-preview/transforms/file', + }, + transformIgnorePatterns: ['/node_modules/'], }; diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ef4c915328c2..eaac48a8f5f0 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -150,7 +150,7 @@ "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, @@ -158,6 +158,11 @@ "webpack>events": true } }, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": { "globals": { "Headers": true, @@ -182,16 +187,28 @@ "crypto": true }, "packages": { + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true, - "@metamask/message-signing-snap>@noble/curves": true, - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/message-signing-snap>@noble/curves": true, - "@metamask/utils>@scure/base": true, - "@noble/hashes": true + "@metamask/utils>@scure/base": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethersproject/abi": { @@ -358,7 +375,7 @@ "@ethereumjs/tx": true, "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, + "@metamask/obs-store": true, "browserify>buffer": true, "ethereumjs-util>rlp": true, "uuid": true, @@ -381,54 +398,6 @@ "TextEncoder": true } }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": true, - "browserify>util": true, - "process": true, - "watchify>xtend": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>isarray": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>timers-browserify": true, - "process": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -621,22 +590,52 @@ }, "@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, + "@metamask/abi-utils>@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/accounts-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, + "@metamask/accounts-controller>@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/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -661,23 +660,9 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>@metamask/rpc-errors": true, - "@metamask/approval-controller>nanoid": true - } - }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/approval-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/approval-controller>nanoid": true, + "@metamask/base-controller": true, + "@metamask/rpc-errors": true } }, "@metamask/approval-controller>nanoid": { @@ -705,13 +690,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": 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, @@ -733,12 +718,6 @@ "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 @@ -787,30 +766,15 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, - "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -846,7 +810,7 @@ "@ethersproject/providers": true, "@metamask/controller-utils": true, "@metamask/ens-controller>@metamask/base-controller": true, - "@metamask/utils": true, + "@metamask/ens-controller>@metamask/utils": true, "punycode": true } }, @@ -858,53 +822,7 @@ "immer": true } }, - "@metamask/eth-json-rpc-filters": { - "globals": { - "console.error": true - }, - "packages": { - "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, - "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, - "@metamask/eth-json-rpc-filters>async-mutex": true, - "@metamask/safe-event-emitter": true, - "pify": true - } - }, - "@metamask/eth-json-rpc-filters>@metamask/eth-query": { - "packages": { - "@metamask/eth-query>json-rpc-random-id": true, - "watchify>xtend": true - } - }, - "@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/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": { + "@metamask/ens-controller>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -919,12 +837,16 @@ "semver": true } }, - "@metamask/eth-json-rpc-filters>async-mutex": { + "@metamask/eth-json-rpc-filters": { "globals": { - "setTimeout": true + "console.error": true }, "packages": { - "@swc/helpers>tslib": true + "@metamask/eth-query": true, + "@metamask/json-rpc-engine": true, + "@metamask/name-controller>async-mutex": true, + "@metamask/safe-event-emitter": true, + "pify": true } }, "@metamask/eth-json-rpc-middleware": { @@ -936,23 +858,38 @@ "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>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, - "@metamask/eth-sig-util": true, - "@metamask/utils": true + "@metamask/eth-sig-util": 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/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-middleware>@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-ledger-bridge-keyring": { @@ -966,14 +903,19 @@ }, "packages": { "@ethereumjs/tx": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": true, "@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, "webpack>events": true } }, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/eth-query": { "packages": { "@metamask/eth-query>json-rpc-random-id": true, @@ -985,12 +927,27 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/abi-utils": true, + "@metamask/eth-sig-util>@metamask/utils": true, "@metamask/eth-sig-util>tweetnacl": true, - "@metamask/utils": true, "@metamask/utils>@scure/base": true, "browserify>buffer": true } }, + "@metamask/eth-sig-util>@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-sig-util>tweetnacl": { "globals": { "crypto": true, @@ -1009,13 +966,28 @@ "packages": { "@ethereumjs/tx": true, "@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/eth-snap-keyring>uuid": true, "@metamask/keyring-api": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true, "webpack>events": true } }, + "@metamask/eth-snap-keyring>@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-snap-keyring>uuid": { "globals": { "crypto": true @@ -1315,24 +1287,24 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller": { + "@metamask/gas-fee-controller>@metamask/base-controller": { "globals": { - "clearTimeout": true, - "console.error": true, "setTimeout": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": true, - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "uuid": true + "immer": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": { + "@metamask/gas-fee-controller>@metamask/polling-controller": { "globals": { + "clearTimeout": true, + "console.error": true, "setTimeout": true }, "packages": { - "immer": true + "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/jazzicon": { @@ -1369,24 +1341,20 @@ }, "@metamask/json-rpc-engine": { "packages": { - "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "@metamask/utils": true } }, - "@metamask/json-rpc-engine>@metamask/utils": { + "@metamask/json-rpc-middleware-stream": { "globals": { - "TextDecoder": true, - "TextEncoder": true + "console.warn": true, + "setTimeout": 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/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true } }, "@metamask/keyring-api": { @@ -1394,28 +1362,43 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, - "@metamask/keyring-api>uuid": { + "@metamask/keyring-api>@metamask/utils": { "globals": { - "crypto": true - } - }, - "@metamask/keyring-controller": { - "packages": { + "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/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, + "@metamask/keyring-controller": { + "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/keyring-controller>@metamask/utils": true, "@metamask/keyring-controller>ethereumjs-wallet": true, - "@metamask/name-controller>async-mutex": true, - "@metamask/utils": true + "@metamask/name-controller>async-mutex": true } }, "@metamask/keyring-controller>@metamask/eth-hd-keyring": { @@ -1426,21 +1409,66 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@metamask/utils": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, "browserify>buffer": true } }, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@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/keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, - "@metamask/utils": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@metamask/utils": true, "browserify>buffer": true, "crypto-browserify>randombytes": true } }, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@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/keyring-controller>@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/keyring-controller>ethereumjs-wallet": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, @@ -1504,13 +1532,28 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/eth-sig-util": true, + "@metamask/message-manager>@metamask/utils": true, "@metamask/message-manager>jsonschema": true, - "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/message-manager>@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/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1528,7 +1571,13 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": true + } + }, + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@metamask/name-controller": { @@ -1591,8 +1640,8 @@ "@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>@metamask/utils": true, "@metamask/network-controller>reselect": true, - "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, "uri-js": true, @@ -1731,27 +1780,57 @@ "@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/utils": true, + "@metamask/safe-event-emitter": 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/eth-json-rpc-provider>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@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/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@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>reselect": { @@ -1808,6 +1887,7 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, + "@metamask/notification-services-controller>@metamask/utils": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1820,6 +1900,21 @@ "SuppressedError": true } }, + "@metamask/notification-services-controller>@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/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2081,9 +2176,21 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/phishing-controller>fastest-levenshtein": true, "@noble/hashes": true, - "punycode": true + "punycode": true, + "webpack-cli>fastest-levenshtein": true + } + }, + "@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/post-message-stream": { @@ -2098,10 +2205,25 @@ "removeEventListener": true }, "packages": { - "@metamask/utils": true, + "@metamask/post-message-stream>@metamask/utils": true, "readable-stream": true } }, + "@metamask/post-message-stream>@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/ppom-validator": { "globals": { "URL": true, @@ -2229,24 +2351,9 @@ "packages": { "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true - } - }, - "@metamask/queued-request-controller>@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/selected-network-controller": true, + "@metamask/utils": true } }, "@metamask/rate-limit-controller": { @@ -2305,23 +2412,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@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/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2367,16 +2459,42 @@ "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>jsonschema": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/signature-controller>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/abi-utils": true, + "@metamask/eth-sig-util>tweetnacl": true, + "@metamask/signature-controller>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true + } + }, + "@metamask/signature-controller>@metamask/eth-sig-util>@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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2390,9 +2508,9 @@ "@ethersproject/bytes": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "@metamask/smart-transactions-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller": true, "@metamask/smart-transactions-controller>bignumber.js": true, "browserify>buffer": true, @@ -2400,17 +2518,12 @@ "lodash": true } }, - "@metamask/smart-transactions-controller>@babel/runtime": { - "globals": { - "regeneratorRuntime": "write" - } - }, "@metamask/smart-transactions-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": true, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": true, - "@metamask/smart-transactions-controller>@ethereumjs/util": true + "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true } }, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": { @@ -2419,11 +2532,6 @@ "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@ethereumjs/util": { "globals": { "console.warn": true, @@ -2431,15 +2539,10 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -2453,6 +2556,18 @@ "crypto.getRandomValues": true } }, + "@metamask/smart-transactions-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/smart-transactions-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller": { "globals": { "clearTimeout": true, @@ -2477,9 +2592,9 @@ "@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, "browserify>buffer": true, + "eth-method-registry": true, "fast-json-patch": true, "lodash": true, "uuid": true, @@ -2489,21 +2604,26 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": { "globals": { "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, "webpack>events": true @@ -2528,10 +2648,10 @@ "@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/rpc-errors>@metamask/utils": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -2546,56 +2666,19 @@ "semver": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { - "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": { - "packages": { - "@metamask/ethjs>ethjs-abi": true, - "@metamask/ethjs>js-sha3": true, - "@metamask/smart-transactions-controller>@babel/runtime": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": { - "globals": { - "clearInterval": true, - "setInterval": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": { - "packages": { - "@metamask/ethjs>@metamask/ethjs-util>is-hex-prefixed": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "browserify>buffer": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { - "console": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": { - "packages": { - "@metamask/ethjs-query>@metamask/ethjs-format>ethjs-schema": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "@metamask/ethjs>@metamask/number-to-bn": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": { - "packages": { - "promise-to-callback": true + "@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/smart-transactions-controller>bignumber.js": { @@ -2614,59 +2697,34 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, + "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, "@metamask/post-message-stream": 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/rpc-errors": 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, "@metamask/snaps-controllers>nanoid": true, "@metamask/snaps-controllers>readable-web-to-node-stream": true, - "@metamask/snaps-controllers>tar-stream": true, - "@metamask/snaps-rpc-methods": true, - "@metamask/snaps-sdk": true, - "@metamask/snaps-utils": true, - "@metamask/snaps-utils>@metamask/snaps-registry": true, - "@metamask/utils": true, - "browserify>browserify-zlib": true, - "eslint>fast-deep-equal": true, - "immer": true, - "readable-stream": true - } - }, - "@metamask/snaps-controllers-flask>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": { - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/snaps-controllers>tar-stream": true, + "@metamask/snaps-rpc-methods": true, + "@metamask/snaps-sdk": true, + "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, + "@metamask/utils": true, + "browserify>browserify-zlib": true, + "eslint>fast-deep-equal": true, + "immer": true, + "readable-stream": true, + "semver": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { + "@metamask/snaps-controllers-flask>nanoid": { "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true + "crypto.getRandomValues": true } }, "@metamask/snaps-controllers>@metamask/permission-controller": { @@ -2674,22 +2732,16 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@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, @@ -2747,8 +2799,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, @@ -2762,48 +2814,27 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": 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/json-rpc-engine": true, + "@metamask/rpc-errors": 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 } }, - "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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/snaps-sdk>@metamask/rpc-errors": true, + "@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2812,15 +2843,24 @@ "packages": { "@metamask/message-signing-snap>@noble/curves": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": true, "@metamask/utils>@scure/base": true, "@noble/hashes": true } }, - "@metamask/snaps-sdk>@metamask/rpc-errors": { + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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/snaps-utils": { @@ -2839,10 +2879,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, @@ -2858,47 +2898,26 @@ "semver": true } }, - "@metamask/snaps-utils>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/snaps-utils>@metamask/permission-controller": { "globals": { "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-utils>@metamask/base-controller": true, - "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": 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, @@ -2973,7 +2992,7 @@ "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/utils": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2999,21 +3018,6 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@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": { "globals": { "fetch": true @@ -3022,9 +3026,9 @@ "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/polling-controller": true, "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, @@ -3042,6 +3046,18 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/user-operation-controller>@metamask/base-controller": true, + "uuid": true + } + }, "@metamask/user-operation-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -3357,6 +3373,51 @@ "process": true } }, + "@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@solana/addresses>@solana/assertions": true, + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/codecs-strings": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-core": { + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@storybook/addon-docs>remark-external-links>mdast-util-definitions": { "packages": { "react-markdown>unist-util-visit": true @@ -4077,59 +4138,16 @@ "setInterval": true }, "packages": { + "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "bn.js": true, "browserify>buffer": true, "crypto-browserify": true, - "eth-lattice-keyring>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk": true, "eth-lattice-keyring>rlp": true, "webpack>events": true } }, - "eth-lattice-keyring>@ethereumjs/tx": { - "packages": { - "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": { - "packages": { - "browserify": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>case": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { - "globals": { - "WeakRef": true - }, - "packages": { - "browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "eth-lattice-keyring>gridplus-sdk": { "globals": { "AbortController": true, @@ -4147,67 +4165,59 @@ "packages": { "@ethereumjs/tx>@ethereumjs/common>crc-32": true, "@ethersproject/abi": true, + "@metamask/eth-sig-util": true, "@metamask/ethjs>js-sha3": true, "@metamask/keyring-api>bech32": true, + "@metamask/ppom-validator>elliptic": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk>aes-js": true, "eth-lattice-keyring>gridplus-sdk>bignumber.js": true, - "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, - "eth-lattice-keyring>gridplus-sdk>elliptic": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, - "eth-lattice-keyring>gridplus-sdk>rlp": true, + "eth-lattice-keyring>gridplus-sdk>bs58check": true, + "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, - "ganache>secp256k1": true, "lodash": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": { "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": true + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": { + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": { "globals": { - "TextDecoder": true, - "crypto": true + "console.warn": true, + "fetch": true }, "packages": { - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, + "webpack>events": true } }, "eth-lattice-keyring>gridplus-sdk>aes-js": { @@ -4221,11 +4231,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4247,33 +4252,24 @@ "globals": { "URL": true, "URLSearchParams": true, - "location": true + "location": true, + "navigator": true } }, - "eth-lattice-keyring>gridplus-sdk>elliptic": { + "eth-lattice-keyring>gridplus-sdk>bs58check": { "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "@noble/hashes": true, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": true } }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": { - "globals": { - "intToBuffer": true - }, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": { "packages": { - "@metamask/ethjs>js-sha3": true, - "bn.js": true, - "buffer": true + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58>base-x": true } }, - "eth-lattice-keyring>gridplus-sdk>rlp": { - "globals": { - "TextEncoder": true + "eth-lattice-keyring>gridplus-sdk>secp256k1": { + "packages": { + "@metamask/ppom-validator>elliptic": true } }, "eth-lattice-keyring>gridplus-sdk>uuid": { @@ -4597,20 +4593,9 @@ "ethers>@ethersproject/signing-key": { "packages": { "@ethersproject/bytes": true, + "@metamask/ppom-validator>elliptic": true, "ethers>@ethersproject/logger": true, - "ethers>@ethersproject/properties": true, - "ethers>@ethersproject/signing-key>elliptic": true - } - }, - "ethers>@ethersproject/signing-key>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "ethers>@ethersproject/properties": true } }, "ethers>@ethersproject/solidity": { @@ -4743,16 +4728,6 @@ "stream-http": true } }, - "json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "readable-stream": true - } - }, "koa>content-disposition>safe-buffer": { "packages": { "browserify>buffer": true @@ -5269,10 +5244,10 @@ "document": true }, "packages": { + "@babel/runtime": true, "prop-types": true, "react": true, "react-dom": true, - "react-redux>@babel/runtime": true, "react-redux>hoist-non-react-statics": true, "react-redux>react-is": true } @@ -5496,16 +5471,6 @@ "webpack>events": true } }, - "readable-stream-2>core-util-is": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "readable-stream-2>process-nextick-args": { - "packages": { - "process": true - } - }, "readable-stream>util-deprecate": { "globals": { "console.trace": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ef4c915328c2..eaac48a8f5f0 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -150,7 +150,7 @@ "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, @@ -158,6 +158,11 @@ "webpack>events": true } }, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": { "globals": { "Headers": true, @@ -182,16 +187,28 @@ "crypto": true }, "packages": { + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true, - "@metamask/message-signing-snap>@noble/curves": true, - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/message-signing-snap>@noble/curves": true, - "@metamask/utils>@scure/base": true, - "@noble/hashes": true + "@metamask/utils>@scure/base": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethersproject/abi": { @@ -358,7 +375,7 @@ "@ethereumjs/tx": true, "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, + "@metamask/obs-store": true, "browserify>buffer": true, "ethereumjs-util>rlp": true, "uuid": true, @@ -381,54 +398,6 @@ "TextEncoder": true } }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": true, - "browserify>util": true, - "process": true, - "watchify>xtend": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>isarray": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>timers-browserify": true, - "process": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -621,22 +590,52 @@ }, "@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, + "@metamask/abi-utils>@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/accounts-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, + "@metamask/accounts-controller>@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/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -661,23 +660,9 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>@metamask/rpc-errors": true, - "@metamask/approval-controller>nanoid": true - } - }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/approval-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/approval-controller>nanoid": true, + "@metamask/base-controller": true, + "@metamask/rpc-errors": true } }, "@metamask/approval-controller>nanoid": { @@ -705,13 +690,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": 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, @@ -733,12 +718,6 @@ "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 @@ -787,30 +766,15 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, - "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -846,7 +810,7 @@ "@ethersproject/providers": true, "@metamask/controller-utils": true, "@metamask/ens-controller>@metamask/base-controller": true, - "@metamask/utils": true, + "@metamask/ens-controller>@metamask/utils": true, "punycode": true } }, @@ -858,53 +822,7 @@ "immer": true } }, - "@metamask/eth-json-rpc-filters": { - "globals": { - "console.error": true - }, - "packages": { - "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, - "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, - "@metamask/eth-json-rpc-filters>async-mutex": true, - "@metamask/safe-event-emitter": true, - "pify": true - } - }, - "@metamask/eth-json-rpc-filters>@metamask/eth-query": { - "packages": { - "@metamask/eth-query>json-rpc-random-id": true, - "watchify>xtend": true - } - }, - "@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/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": { + "@metamask/ens-controller>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -919,12 +837,16 @@ "semver": true } }, - "@metamask/eth-json-rpc-filters>async-mutex": { + "@metamask/eth-json-rpc-filters": { "globals": { - "setTimeout": true + "console.error": true }, "packages": { - "@swc/helpers>tslib": true + "@metamask/eth-query": true, + "@metamask/json-rpc-engine": true, + "@metamask/name-controller>async-mutex": true, + "@metamask/safe-event-emitter": true, + "pify": true } }, "@metamask/eth-json-rpc-middleware": { @@ -936,23 +858,38 @@ "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>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, - "@metamask/eth-sig-util": true, - "@metamask/utils": true + "@metamask/eth-sig-util": 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/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-middleware>@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-ledger-bridge-keyring": { @@ -966,14 +903,19 @@ }, "packages": { "@ethereumjs/tx": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": true, "@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, "webpack>events": true } }, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/eth-query": { "packages": { "@metamask/eth-query>json-rpc-random-id": true, @@ -985,12 +927,27 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/abi-utils": true, + "@metamask/eth-sig-util>@metamask/utils": true, "@metamask/eth-sig-util>tweetnacl": true, - "@metamask/utils": true, "@metamask/utils>@scure/base": true, "browserify>buffer": true } }, + "@metamask/eth-sig-util>@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-sig-util>tweetnacl": { "globals": { "crypto": true, @@ -1009,13 +966,28 @@ "packages": { "@ethereumjs/tx": true, "@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/eth-snap-keyring>uuid": true, "@metamask/keyring-api": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true, "webpack>events": true } }, + "@metamask/eth-snap-keyring>@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-snap-keyring>uuid": { "globals": { "crypto": true @@ -1315,24 +1287,24 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller": { + "@metamask/gas-fee-controller>@metamask/base-controller": { "globals": { - "clearTimeout": true, - "console.error": true, "setTimeout": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": true, - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "uuid": true + "immer": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": { + "@metamask/gas-fee-controller>@metamask/polling-controller": { "globals": { + "clearTimeout": true, + "console.error": true, "setTimeout": true }, "packages": { - "immer": true + "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/jazzicon": { @@ -1369,24 +1341,20 @@ }, "@metamask/json-rpc-engine": { "packages": { - "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "@metamask/utils": true } }, - "@metamask/json-rpc-engine>@metamask/utils": { + "@metamask/json-rpc-middleware-stream": { "globals": { - "TextDecoder": true, - "TextEncoder": true + "console.warn": true, + "setTimeout": 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/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true } }, "@metamask/keyring-api": { @@ -1394,28 +1362,43 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, - "@metamask/keyring-api>uuid": { + "@metamask/keyring-api>@metamask/utils": { "globals": { - "crypto": true - } - }, - "@metamask/keyring-controller": { - "packages": { + "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/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, + "@metamask/keyring-controller": { + "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/keyring-controller>@metamask/utils": true, "@metamask/keyring-controller>ethereumjs-wallet": true, - "@metamask/name-controller>async-mutex": true, - "@metamask/utils": true + "@metamask/name-controller>async-mutex": true } }, "@metamask/keyring-controller>@metamask/eth-hd-keyring": { @@ -1426,21 +1409,66 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@metamask/utils": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, "browserify>buffer": true } }, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@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/keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, - "@metamask/utils": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@metamask/utils": true, "browserify>buffer": true, "crypto-browserify>randombytes": true } }, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@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/keyring-controller>@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/keyring-controller>ethereumjs-wallet": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, @@ -1504,13 +1532,28 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/eth-sig-util": true, + "@metamask/message-manager>@metamask/utils": true, "@metamask/message-manager>jsonschema": true, - "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/message-manager>@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/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1528,7 +1571,13 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": true + } + }, + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@metamask/name-controller": { @@ -1591,8 +1640,8 @@ "@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>@metamask/utils": true, "@metamask/network-controller>reselect": true, - "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, "uri-js": true, @@ -1731,27 +1780,57 @@ "@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/utils": true, + "@metamask/safe-event-emitter": 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/eth-json-rpc-provider>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@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/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@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>reselect": { @@ -1808,6 +1887,7 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, + "@metamask/notification-services-controller>@metamask/utils": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1820,6 +1900,21 @@ "SuppressedError": true } }, + "@metamask/notification-services-controller>@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/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2081,9 +2176,21 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/phishing-controller>fastest-levenshtein": true, "@noble/hashes": true, - "punycode": true + "punycode": true, + "webpack-cli>fastest-levenshtein": true + } + }, + "@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/post-message-stream": { @@ -2098,10 +2205,25 @@ "removeEventListener": true }, "packages": { - "@metamask/utils": true, + "@metamask/post-message-stream>@metamask/utils": true, "readable-stream": true } }, + "@metamask/post-message-stream>@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/ppom-validator": { "globals": { "URL": true, @@ -2229,24 +2351,9 @@ "packages": { "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true - } - }, - "@metamask/queued-request-controller>@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/selected-network-controller": true, + "@metamask/utils": true } }, "@metamask/rate-limit-controller": { @@ -2305,23 +2412,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@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/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2367,16 +2459,42 @@ "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>jsonschema": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/signature-controller>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/abi-utils": true, + "@metamask/eth-sig-util>tweetnacl": true, + "@metamask/signature-controller>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true + } + }, + "@metamask/signature-controller>@metamask/eth-sig-util>@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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2390,9 +2508,9 @@ "@ethersproject/bytes": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "@metamask/smart-transactions-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller": true, "@metamask/smart-transactions-controller>bignumber.js": true, "browserify>buffer": true, @@ -2400,17 +2518,12 @@ "lodash": true } }, - "@metamask/smart-transactions-controller>@babel/runtime": { - "globals": { - "regeneratorRuntime": "write" - } - }, "@metamask/smart-transactions-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": true, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": true, - "@metamask/smart-transactions-controller>@ethereumjs/util": true + "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true } }, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": { @@ -2419,11 +2532,6 @@ "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@ethereumjs/util": { "globals": { "console.warn": true, @@ -2431,15 +2539,10 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -2453,6 +2556,18 @@ "crypto.getRandomValues": true } }, + "@metamask/smart-transactions-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/smart-transactions-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller": { "globals": { "clearTimeout": true, @@ -2477,9 +2592,9 @@ "@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, "browserify>buffer": true, + "eth-method-registry": true, "fast-json-patch": true, "lodash": true, "uuid": true, @@ -2489,21 +2604,26 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": { "globals": { "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, "webpack>events": true @@ -2528,10 +2648,10 @@ "@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/rpc-errors>@metamask/utils": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -2546,56 +2666,19 @@ "semver": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { - "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": { - "packages": { - "@metamask/ethjs>ethjs-abi": true, - "@metamask/ethjs>js-sha3": true, - "@metamask/smart-transactions-controller>@babel/runtime": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": { - "globals": { - "clearInterval": true, - "setInterval": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": { - "packages": { - "@metamask/ethjs>@metamask/ethjs-util>is-hex-prefixed": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "browserify>buffer": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { - "console": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": { - "packages": { - "@metamask/ethjs-query>@metamask/ethjs-format>ethjs-schema": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "@metamask/ethjs>@metamask/number-to-bn": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": { - "packages": { - "promise-to-callback": true + "@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/smart-transactions-controller>bignumber.js": { @@ -2614,59 +2697,34 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, + "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, "@metamask/post-message-stream": 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/rpc-errors": 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, "@metamask/snaps-controllers>nanoid": true, "@metamask/snaps-controllers>readable-web-to-node-stream": true, - "@metamask/snaps-controllers>tar-stream": true, - "@metamask/snaps-rpc-methods": true, - "@metamask/snaps-sdk": true, - "@metamask/snaps-utils": true, - "@metamask/snaps-utils>@metamask/snaps-registry": true, - "@metamask/utils": true, - "browserify>browserify-zlib": true, - "eslint>fast-deep-equal": true, - "immer": true, - "readable-stream": true - } - }, - "@metamask/snaps-controllers-flask>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": { - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/snaps-controllers>tar-stream": true, + "@metamask/snaps-rpc-methods": true, + "@metamask/snaps-sdk": true, + "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, + "@metamask/utils": true, + "browserify>browserify-zlib": true, + "eslint>fast-deep-equal": true, + "immer": true, + "readable-stream": true, + "semver": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { + "@metamask/snaps-controllers-flask>nanoid": { "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true + "crypto.getRandomValues": true } }, "@metamask/snaps-controllers>@metamask/permission-controller": { @@ -2674,22 +2732,16 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@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, @@ -2747,8 +2799,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, @@ -2762,48 +2814,27 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": 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/json-rpc-engine": true, + "@metamask/rpc-errors": 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 } }, - "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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/snaps-sdk>@metamask/rpc-errors": true, + "@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2812,15 +2843,24 @@ "packages": { "@metamask/message-signing-snap>@noble/curves": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": true, "@metamask/utils>@scure/base": true, "@noble/hashes": true } }, - "@metamask/snaps-sdk>@metamask/rpc-errors": { + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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/snaps-utils": { @@ -2839,10 +2879,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, @@ -2858,47 +2898,26 @@ "semver": true } }, - "@metamask/snaps-utils>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/snaps-utils>@metamask/permission-controller": { "globals": { "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-utils>@metamask/base-controller": true, - "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": 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, @@ -2973,7 +2992,7 @@ "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/utils": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2999,21 +3018,6 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@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": { "globals": { "fetch": true @@ -3022,9 +3026,9 @@ "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/polling-controller": true, "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, @@ -3042,6 +3046,18 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/user-operation-controller>@metamask/base-controller": true, + "uuid": true + } + }, "@metamask/user-operation-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -3357,6 +3373,51 @@ "process": true } }, + "@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@solana/addresses>@solana/assertions": true, + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/codecs-strings": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-core": { + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@storybook/addon-docs>remark-external-links>mdast-util-definitions": { "packages": { "react-markdown>unist-util-visit": true @@ -4077,59 +4138,16 @@ "setInterval": true }, "packages": { + "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "bn.js": true, "browserify>buffer": true, "crypto-browserify": true, - "eth-lattice-keyring>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk": true, "eth-lattice-keyring>rlp": true, "webpack>events": true } }, - "eth-lattice-keyring>@ethereumjs/tx": { - "packages": { - "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": { - "packages": { - "browserify": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>case": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { - "globals": { - "WeakRef": true - }, - "packages": { - "browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "eth-lattice-keyring>gridplus-sdk": { "globals": { "AbortController": true, @@ -4147,67 +4165,59 @@ "packages": { "@ethereumjs/tx>@ethereumjs/common>crc-32": true, "@ethersproject/abi": true, + "@metamask/eth-sig-util": true, "@metamask/ethjs>js-sha3": true, "@metamask/keyring-api>bech32": true, + "@metamask/ppom-validator>elliptic": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk>aes-js": true, "eth-lattice-keyring>gridplus-sdk>bignumber.js": true, - "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, - "eth-lattice-keyring>gridplus-sdk>elliptic": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, - "eth-lattice-keyring>gridplus-sdk>rlp": true, + "eth-lattice-keyring>gridplus-sdk>bs58check": true, + "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, - "ganache>secp256k1": true, "lodash": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": { "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": true + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": { + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": { "globals": { - "TextDecoder": true, - "crypto": true + "console.warn": true, + "fetch": true }, "packages": { - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, + "webpack>events": true } }, "eth-lattice-keyring>gridplus-sdk>aes-js": { @@ -4221,11 +4231,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4247,33 +4252,24 @@ "globals": { "URL": true, "URLSearchParams": true, - "location": true + "location": true, + "navigator": true } }, - "eth-lattice-keyring>gridplus-sdk>elliptic": { + "eth-lattice-keyring>gridplus-sdk>bs58check": { "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "@noble/hashes": true, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": true } }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": { - "globals": { - "intToBuffer": true - }, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": { "packages": { - "@metamask/ethjs>js-sha3": true, - "bn.js": true, - "buffer": true + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58>base-x": true } }, - "eth-lattice-keyring>gridplus-sdk>rlp": { - "globals": { - "TextEncoder": true + "eth-lattice-keyring>gridplus-sdk>secp256k1": { + "packages": { + "@metamask/ppom-validator>elliptic": true } }, "eth-lattice-keyring>gridplus-sdk>uuid": { @@ -4597,20 +4593,9 @@ "ethers>@ethersproject/signing-key": { "packages": { "@ethersproject/bytes": true, + "@metamask/ppom-validator>elliptic": true, "ethers>@ethersproject/logger": true, - "ethers>@ethersproject/properties": true, - "ethers>@ethersproject/signing-key>elliptic": true - } - }, - "ethers>@ethersproject/signing-key>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "ethers>@ethersproject/properties": true } }, "ethers>@ethersproject/solidity": { @@ -4743,16 +4728,6 @@ "stream-http": true } }, - "json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "readable-stream": true - } - }, "koa>content-disposition>safe-buffer": { "packages": { "browserify>buffer": true @@ -5269,10 +5244,10 @@ "document": true }, "packages": { + "@babel/runtime": true, "prop-types": true, "react": true, "react-dom": true, - "react-redux>@babel/runtime": true, "react-redux>hoist-non-react-statics": true, "react-redux>react-is": true } @@ -5496,16 +5471,6 @@ "webpack>events": true } }, - "readable-stream-2>core-util-is": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "readable-stream-2>process-nextick-args": { - "packages": { - "process": true - } - }, "readable-stream>util-deprecate": { "globals": { "console.trace": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ef4c915328c2..eaac48a8f5f0 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -150,7 +150,7 @@ "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, @@ -158,6 +158,11 @@ "webpack>events": true } }, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": { "globals": { "Headers": true, @@ -182,16 +187,28 @@ "crypto": true }, "packages": { + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true, - "@metamask/message-signing-snap>@noble/curves": true, - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/message-signing-snap>@noble/curves": true, - "@metamask/utils>@scure/base": true, - "@noble/hashes": true + "@metamask/utils>@scure/base": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethersproject/abi": { @@ -358,7 +375,7 @@ "@ethereumjs/tx": true, "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, + "@metamask/obs-store": true, "browserify>buffer": true, "ethereumjs-util>rlp": true, "uuid": true, @@ -381,54 +398,6 @@ "TextEncoder": true } }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": true, - "browserify>util": true, - "process": true, - "watchify>xtend": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>isarray": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>timers-browserify": true, - "process": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -621,22 +590,52 @@ }, "@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, + "@metamask/abi-utils>@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/accounts-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, + "@metamask/accounts-controller>@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/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -661,23 +660,9 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>@metamask/rpc-errors": true, - "@metamask/approval-controller>nanoid": true - } - }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/approval-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/approval-controller>nanoid": true, + "@metamask/base-controller": true, + "@metamask/rpc-errors": true } }, "@metamask/approval-controller>nanoid": { @@ -705,13 +690,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": 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, @@ -733,12 +718,6 @@ "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 @@ -787,30 +766,15 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, - "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -846,7 +810,7 @@ "@ethersproject/providers": true, "@metamask/controller-utils": true, "@metamask/ens-controller>@metamask/base-controller": true, - "@metamask/utils": true, + "@metamask/ens-controller>@metamask/utils": true, "punycode": true } }, @@ -858,53 +822,7 @@ "immer": true } }, - "@metamask/eth-json-rpc-filters": { - "globals": { - "console.error": true - }, - "packages": { - "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, - "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, - "@metamask/eth-json-rpc-filters>async-mutex": true, - "@metamask/safe-event-emitter": true, - "pify": true - } - }, - "@metamask/eth-json-rpc-filters>@metamask/eth-query": { - "packages": { - "@metamask/eth-query>json-rpc-random-id": true, - "watchify>xtend": true - } - }, - "@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/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": { + "@metamask/ens-controller>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -919,12 +837,16 @@ "semver": true } }, - "@metamask/eth-json-rpc-filters>async-mutex": { + "@metamask/eth-json-rpc-filters": { "globals": { - "setTimeout": true + "console.error": true }, "packages": { - "@swc/helpers>tslib": true + "@metamask/eth-query": true, + "@metamask/json-rpc-engine": true, + "@metamask/name-controller>async-mutex": true, + "@metamask/safe-event-emitter": true, + "pify": true } }, "@metamask/eth-json-rpc-middleware": { @@ -936,23 +858,38 @@ "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>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, - "@metamask/eth-sig-util": true, - "@metamask/utils": true + "@metamask/eth-sig-util": 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/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-middleware>@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-ledger-bridge-keyring": { @@ -966,14 +903,19 @@ }, "packages": { "@ethereumjs/tx": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": true, "@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, "webpack>events": true } }, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/eth-query": { "packages": { "@metamask/eth-query>json-rpc-random-id": true, @@ -985,12 +927,27 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/abi-utils": true, + "@metamask/eth-sig-util>@metamask/utils": true, "@metamask/eth-sig-util>tweetnacl": true, - "@metamask/utils": true, "@metamask/utils>@scure/base": true, "browserify>buffer": true } }, + "@metamask/eth-sig-util>@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-sig-util>tweetnacl": { "globals": { "crypto": true, @@ -1009,13 +966,28 @@ "packages": { "@ethereumjs/tx": true, "@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/eth-snap-keyring>uuid": true, "@metamask/keyring-api": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true, "webpack>events": true } }, + "@metamask/eth-snap-keyring>@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-snap-keyring>uuid": { "globals": { "crypto": true @@ -1315,24 +1287,24 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller": { + "@metamask/gas-fee-controller>@metamask/base-controller": { "globals": { - "clearTimeout": true, - "console.error": true, "setTimeout": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": true, - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "uuid": true + "immer": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": { + "@metamask/gas-fee-controller>@metamask/polling-controller": { "globals": { + "clearTimeout": true, + "console.error": true, "setTimeout": true }, "packages": { - "immer": true + "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/jazzicon": { @@ -1369,24 +1341,20 @@ }, "@metamask/json-rpc-engine": { "packages": { - "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "@metamask/utils": true } }, - "@metamask/json-rpc-engine>@metamask/utils": { + "@metamask/json-rpc-middleware-stream": { "globals": { - "TextDecoder": true, - "TextEncoder": true + "console.warn": true, + "setTimeout": 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/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true } }, "@metamask/keyring-api": { @@ -1394,28 +1362,43 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, - "@metamask/keyring-api>uuid": { + "@metamask/keyring-api>@metamask/utils": { "globals": { - "crypto": true - } - }, - "@metamask/keyring-controller": { - "packages": { + "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/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, + "@metamask/keyring-controller": { + "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/keyring-controller>@metamask/utils": true, "@metamask/keyring-controller>ethereumjs-wallet": true, - "@metamask/name-controller>async-mutex": true, - "@metamask/utils": true + "@metamask/name-controller>async-mutex": true } }, "@metamask/keyring-controller>@metamask/eth-hd-keyring": { @@ -1426,21 +1409,66 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@metamask/utils": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, "browserify>buffer": true } }, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@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/keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, - "@metamask/utils": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@metamask/utils": true, "browserify>buffer": true, "crypto-browserify>randombytes": true } }, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@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/keyring-controller>@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/keyring-controller>ethereumjs-wallet": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, @@ -1504,13 +1532,28 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/eth-sig-util": true, + "@metamask/message-manager>@metamask/utils": true, "@metamask/message-manager>jsonschema": true, - "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/message-manager>@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/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1528,7 +1571,13 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": true + } + }, + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@metamask/name-controller": { @@ -1591,8 +1640,8 @@ "@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>@metamask/utils": true, "@metamask/network-controller>reselect": true, - "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, "uri-js": true, @@ -1731,27 +1780,57 @@ "@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/utils": true, + "@metamask/safe-event-emitter": 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/eth-json-rpc-provider>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@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/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@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>reselect": { @@ -1808,6 +1887,7 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, + "@metamask/notification-services-controller>@metamask/utils": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1820,6 +1900,21 @@ "SuppressedError": true } }, + "@metamask/notification-services-controller>@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/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2081,9 +2176,21 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/phishing-controller>fastest-levenshtein": true, "@noble/hashes": true, - "punycode": true + "punycode": true, + "webpack-cli>fastest-levenshtein": true + } + }, + "@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/post-message-stream": { @@ -2098,10 +2205,25 @@ "removeEventListener": true }, "packages": { - "@metamask/utils": true, + "@metamask/post-message-stream>@metamask/utils": true, "readable-stream": true } }, + "@metamask/post-message-stream>@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/ppom-validator": { "globals": { "URL": true, @@ -2229,24 +2351,9 @@ "packages": { "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true - } - }, - "@metamask/queued-request-controller>@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/selected-network-controller": true, + "@metamask/utils": true } }, "@metamask/rate-limit-controller": { @@ -2305,23 +2412,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@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/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2367,16 +2459,42 @@ "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>jsonschema": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/signature-controller>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/abi-utils": true, + "@metamask/eth-sig-util>tweetnacl": true, + "@metamask/signature-controller>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true + } + }, + "@metamask/signature-controller>@metamask/eth-sig-util>@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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2390,9 +2508,9 @@ "@ethersproject/bytes": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "@metamask/smart-transactions-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller": true, "@metamask/smart-transactions-controller>bignumber.js": true, "browserify>buffer": true, @@ -2400,17 +2518,12 @@ "lodash": true } }, - "@metamask/smart-transactions-controller>@babel/runtime": { - "globals": { - "regeneratorRuntime": "write" - } - }, "@metamask/smart-transactions-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": true, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": true, - "@metamask/smart-transactions-controller>@ethereumjs/util": true + "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true } }, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": { @@ -2419,11 +2532,6 @@ "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@ethereumjs/util": { "globals": { "console.warn": true, @@ -2431,15 +2539,10 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -2453,6 +2556,18 @@ "crypto.getRandomValues": true } }, + "@metamask/smart-transactions-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/smart-transactions-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller": { "globals": { "clearTimeout": true, @@ -2477,9 +2592,9 @@ "@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, "browserify>buffer": true, + "eth-method-registry": true, "fast-json-patch": true, "lodash": true, "uuid": true, @@ -2489,21 +2604,26 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": { "globals": { "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, "webpack>events": true @@ -2528,10 +2648,10 @@ "@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/rpc-errors>@metamask/utils": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -2546,56 +2666,19 @@ "semver": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { - "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": { - "packages": { - "@metamask/ethjs>ethjs-abi": true, - "@metamask/ethjs>js-sha3": true, - "@metamask/smart-transactions-controller>@babel/runtime": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": { - "globals": { - "clearInterval": true, - "setInterval": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": { - "packages": { - "@metamask/ethjs>@metamask/ethjs-util>is-hex-prefixed": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "browserify>buffer": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { - "console": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": { - "packages": { - "@metamask/ethjs-query>@metamask/ethjs-format>ethjs-schema": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "@metamask/ethjs>@metamask/number-to-bn": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": { - "packages": { - "promise-to-callback": true + "@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/smart-transactions-controller>bignumber.js": { @@ -2614,59 +2697,34 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, + "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, "@metamask/post-message-stream": 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/rpc-errors": 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, "@metamask/snaps-controllers>nanoid": true, "@metamask/snaps-controllers>readable-web-to-node-stream": true, - "@metamask/snaps-controllers>tar-stream": true, - "@metamask/snaps-rpc-methods": true, - "@metamask/snaps-sdk": true, - "@metamask/snaps-utils": true, - "@metamask/snaps-utils>@metamask/snaps-registry": true, - "@metamask/utils": true, - "browserify>browserify-zlib": true, - "eslint>fast-deep-equal": true, - "immer": true, - "readable-stream": true - } - }, - "@metamask/snaps-controllers-flask>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": { - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/snaps-controllers>tar-stream": true, + "@metamask/snaps-rpc-methods": true, + "@metamask/snaps-sdk": true, + "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, + "@metamask/utils": true, + "browserify>browserify-zlib": true, + "eslint>fast-deep-equal": true, + "immer": true, + "readable-stream": true, + "semver": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { + "@metamask/snaps-controllers-flask>nanoid": { "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true + "crypto.getRandomValues": true } }, "@metamask/snaps-controllers>@metamask/permission-controller": { @@ -2674,22 +2732,16 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@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, @@ -2747,8 +2799,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, @@ -2762,48 +2814,27 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": 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/json-rpc-engine": true, + "@metamask/rpc-errors": 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 } }, - "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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/snaps-sdk>@metamask/rpc-errors": true, + "@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2812,15 +2843,24 @@ "packages": { "@metamask/message-signing-snap>@noble/curves": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": true, "@metamask/utils>@scure/base": true, "@noble/hashes": true } }, - "@metamask/snaps-sdk>@metamask/rpc-errors": { + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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/snaps-utils": { @@ -2839,10 +2879,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, @@ -2858,47 +2898,26 @@ "semver": true } }, - "@metamask/snaps-utils>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/snaps-utils>@metamask/permission-controller": { "globals": { "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-utils>@metamask/base-controller": true, - "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": 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, @@ -2973,7 +2992,7 @@ "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/utils": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2999,21 +3018,6 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@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": { "globals": { "fetch": true @@ -3022,9 +3026,9 @@ "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/polling-controller": true, "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, @@ -3042,6 +3046,18 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/user-operation-controller>@metamask/base-controller": true, + "uuid": true + } + }, "@metamask/user-operation-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -3357,6 +3373,51 @@ "process": true } }, + "@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@solana/addresses>@solana/assertions": true, + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/codecs-strings": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-core": { + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@storybook/addon-docs>remark-external-links>mdast-util-definitions": { "packages": { "react-markdown>unist-util-visit": true @@ -4077,59 +4138,16 @@ "setInterval": true }, "packages": { + "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "bn.js": true, "browserify>buffer": true, "crypto-browserify": true, - "eth-lattice-keyring>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk": true, "eth-lattice-keyring>rlp": true, "webpack>events": true } }, - "eth-lattice-keyring>@ethereumjs/tx": { - "packages": { - "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": { - "packages": { - "browserify": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>case": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { - "globals": { - "WeakRef": true - }, - "packages": { - "browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "eth-lattice-keyring>gridplus-sdk": { "globals": { "AbortController": true, @@ -4147,67 +4165,59 @@ "packages": { "@ethereumjs/tx>@ethereumjs/common>crc-32": true, "@ethersproject/abi": true, + "@metamask/eth-sig-util": true, "@metamask/ethjs>js-sha3": true, "@metamask/keyring-api>bech32": true, + "@metamask/ppom-validator>elliptic": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk>aes-js": true, "eth-lattice-keyring>gridplus-sdk>bignumber.js": true, - "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, - "eth-lattice-keyring>gridplus-sdk>elliptic": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, - "eth-lattice-keyring>gridplus-sdk>rlp": true, + "eth-lattice-keyring>gridplus-sdk>bs58check": true, + "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, - "ganache>secp256k1": true, "lodash": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": { "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": true + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": { + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": { "globals": { - "TextDecoder": true, - "crypto": true + "console.warn": true, + "fetch": true }, "packages": { - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, + "webpack>events": true } }, "eth-lattice-keyring>gridplus-sdk>aes-js": { @@ -4221,11 +4231,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4247,33 +4252,24 @@ "globals": { "URL": true, "URLSearchParams": true, - "location": true + "location": true, + "navigator": true } }, - "eth-lattice-keyring>gridplus-sdk>elliptic": { + "eth-lattice-keyring>gridplus-sdk>bs58check": { "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "@noble/hashes": true, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": true } }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": { - "globals": { - "intToBuffer": true - }, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": { "packages": { - "@metamask/ethjs>js-sha3": true, - "bn.js": true, - "buffer": true + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58>base-x": true } }, - "eth-lattice-keyring>gridplus-sdk>rlp": { - "globals": { - "TextEncoder": true + "eth-lattice-keyring>gridplus-sdk>secp256k1": { + "packages": { + "@metamask/ppom-validator>elliptic": true } }, "eth-lattice-keyring>gridplus-sdk>uuid": { @@ -4597,20 +4593,9 @@ "ethers>@ethersproject/signing-key": { "packages": { "@ethersproject/bytes": true, + "@metamask/ppom-validator>elliptic": true, "ethers>@ethersproject/logger": true, - "ethers>@ethersproject/properties": true, - "ethers>@ethersproject/signing-key>elliptic": true - } - }, - "ethers>@ethersproject/signing-key>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "ethers>@ethersproject/properties": true } }, "ethers>@ethersproject/solidity": { @@ -4743,16 +4728,6 @@ "stream-http": true } }, - "json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "readable-stream": true - } - }, "koa>content-disposition>safe-buffer": { "packages": { "browserify>buffer": true @@ -5269,10 +5244,10 @@ "document": true }, "packages": { + "@babel/runtime": true, "prop-types": true, "react": true, "react-dom": true, - "react-redux>@babel/runtime": true, "react-redux>hoist-non-react-statics": true, "react-redux>react-is": true } @@ -5496,16 +5471,6 @@ "webpack>events": true } }, - "readable-stream-2>core-util-is": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "readable-stream-2>process-nextick-args": { - "packages": { - "process": true - } - }, "readable-stream>util-deprecate": { "globals": { "console.trace": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 94c331a71ee5..467ef781215c 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -150,7 +150,7 @@ "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, @@ -158,6 +158,11 @@ "webpack>events": true } }, + "@ethereumjs/tx>@ethereumjs/util>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": { "globals": { "Headers": true, @@ -182,16 +187,28 @@ "crypto": true }, "packages": { + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true, - "@metamask/message-signing-snap>@noble/curves": true, - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { "packages": { + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": true, "@metamask/message-signing-snap>@noble/curves": true, - "@metamask/utils>@scure/base": true, - "@noble/hashes": true + "@metamask/utils>@scure/base": true + } + }, + "@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@ethersproject/abi": { @@ -358,7 +375,7 @@ "@ethereumjs/tx": true, "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, + "@metamask/obs-store": true, "browserify>buffer": true, "ethereumjs-util>rlp": true, "uuid": true, @@ -381,54 +398,6 @@ "TextEncoder": true } }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": true, - "browserify>util": true, - "process": true, - "watchify>xtend": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>isarray": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>timers-browserify": true, - "process": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -713,22 +682,52 @@ }, "@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, + "@metamask/abi-utils>@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/accounts-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, + "@metamask/accounts-controller>@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/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -753,23 +752,9 @@ "console.info": true }, "packages": { - "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>@metamask/rpc-errors": true, - "@metamask/approval-controller>nanoid": true - } - }, - "@metamask/approval-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/approval-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/approval-controller>nanoid": true, + "@metamask/base-controller": true, + "@metamask/rpc-errors": true } }, "@metamask/approval-controller>nanoid": { @@ -797,13 +782,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": 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, @@ -825,12 +810,6 @@ "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 @@ -879,30 +858,15 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, - "@metamask/controller-utils>@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/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -938,7 +902,7 @@ "@ethersproject/providers": true, "@metamask/controller-utils": true, "@metamask/ens-controller>@metamask/base-controller": true, - "@metamask/utils": true, + "@metamask/ens-controller>@metamask/utils": true, "punycode": true } }, @@ -950,53 +914,7 @@ "immer": true } }, - "@metamask/eth-json-rpc-filters": { - "globals": { - "console.error": true - }, - "packages": { - "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, - "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, - "@metamask/eth-json-rpc-filters>async-mutex": true, - "@metamask/safe-event-emitter": true, - "pify": true - } - }, - "@metamask/eth-json-rpc-filters>@metamask/eth-query": { - "packages": { - "@metamask/eth-query>json-rpc-random-id": true, - "watchify>xtend": true - } - }, - "@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/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": { + "@metamask/ens-controller>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -1011,12 +929,16 @@ "semver": true } }, - "@metamask/eth-json-rpc-filters>async-mutex": { + "@metamask/eth-json-rpc-filters": { "globals": { - "setTimeout": true + "console.error": true }, "packages": { - "@swc/helpers>tslib": true + "@metamask/eth-query": true, + "@metamask/json-rpc-engine": true, + "@metamask/name-controller>async-mutex": true, + "@metamask/safe-event-emitter": true, + "pify": true } }, "@metamask/eth-json-rpc-middleware": { @@ -1028,23 +950,38 @@ "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>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, - "@metamask/eth-sig-util": true, - "@metamask/utils": true + "@metamask/eth-sig-util": 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/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-middleware>@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-ledger-bridge-keyring": { @@ -1058,14 +995,19 @@ }, "packages": { "@ethereumjs/tx": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": true, "@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, "webpack>events": true } }, + "@metamask/eth-ledger-bridge-keyring>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/eth-query": { "packages": { "@metamask/eth-query>json-rpc-random-id": true, @@ -1077,12 +1019,27 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/abi-utils": true, + "@metamask/eth-sig-util>@metamask/utils": true, "@metamask/eth-sig-util>tweetnacl": true, - "@metamask/utils": true, "@metamask/utils>@scure/base": true, "browserify>buffer": true } }, + "@metamask/eth-sig-util>@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-sig-util>tweetnacl": { "globals": { "crypto": true, @@ -1101,13 +1058,28 @@ "packages": { "@ethereumjs/tx": true, "@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/eth-snap-keyring>uuid": true, "@metamask/keyring-api": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true, "webpack>events": true } }, + "@metamask/eth-snap-keyring>@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-snap-keyring>uuid": { "globals": { "crypto": true @@ -1407,24 +1379,24 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller": { + "@metamask/gas-fee-controller>@metamask/base-controller": { "globals": { - "clearTimeout": true, - "console.error": true, "setTimeout": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": true, - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "uuid": true + "immer": true } }, - "@metamask/gas-fee-controller>@metamask/polling-controller>@metamask/base-controller": { + "@metamask/gas-fee-controller>@metamask/polling-controller": { "globals": { + "clearTimeout": true, + "console.error": true, "setTimeout": true }, "packages": { - "immer": true + "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/jazzicon": { @@ -1461,24 +1433,20 @@ }, "@metamask/json-rpc-engine": { "packages": { - "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "@metamask/utils": true } }, - "@metamask/json-rpc-engine>@metamask/utils": { + "@metamask/json-rpc-middleware-stream": { "globals": { - "TextDecoder": true, - "TextEncoder": true + "console.warn": true, + "setTimeout": 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/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true } }, "@metamask/keyring-api": { @@ -1486,18 +1454,33 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } }, - "@metamask/keyring-api>uuid": { + "@metamask/keyring-api>@metamask/utils": { "globals": { - "crypto": true - } - }, - "@metamask/keyring-controller": { + "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/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, + "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/base-controller": true, @@ -1505,9 +1488,9 @@ "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/keyring-controller>@metamask/utils": true, "@metamask/keyring-controller>ethereumjs-wallet": true, - "@metamask/name-controller>async-mutex": true, - "@metamask/utils": true + "@metamask/name-controller>async-mutex": true } }, "@metamask/keyring-controller>@metamask/eth-hd-keyring": { @@ -1518,21 +1501,66 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@metamask/utils": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, "browserify>buffer": true } }, + "@metamask/keyring-controller>@metamask/eth-hd-keyring>@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/keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-sig-util": true, - "@metamask/utils": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@metamask/utils": true, "browserify>buffer": true, "crypto-browserify>randombytes": true } }, + "@metamask/keyring-controller>@metamask/eth-simple-keyring>@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/keyring-controller>@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/keyring-controller>ethereumjs-wallet": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, @@ -1596,13 +1624,28 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/eth-sig-util": true, + "@metamask/message-manager>@metamask/utils": true, "@metamask/message-manager>jsonschema": true, - "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/message-manager>@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/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1620,7 +1663,13 @@ "TextEncoder": true }, "packages": { - "@noble/hashes": true + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": true + } + }, + "@metamask/message-signing-snap>@noble/curves>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true } }, "@metamask/name-controller": { @@ -1683,8 +1732,8 @@ "@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>@metamask/utils": true, "@metamask/network-controller>reselect": true, - "@metamask/utils": true, "browserify>assert": true, "browserify>util": true, "uri-js": true, @@ -1823,27 +1872,57 @@ "@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/utils": true, + "@metamask/safe-event-emitter": 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/eth-json-rpc-provider>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@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/json-rpc-engine": { "packages": { "@metamask/network-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/safe-event-emitter": true } }, "@metamask/network-controller>@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/network-controller>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@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>reselect": { @@ -1900,6 +1979,7 @@ "@metamask/base-controller": true, "@metamask/controller-utils": true, "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, + "@metamask/notification-services-controller>@metamask/utils": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, "bignumber.js": true, @@ -1912,6 +1992,21 @@ "SuppressedError": true } }, + "@metamask/notification-services-controller>@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/notification-services-controller>firebase": { "packages": { "@metamask/notification-services-controller>firebase>@firebase/app": true, @@ -2173,9 +2268,21 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/phishing-controller>fastest-levenshtein": true, "@noble/hashes": true, - "punycode": true + "punycode": true, + "webpack-cli>fastest-levenshtein": true + } + }, + "@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true } }, "@metamask/post-message-stream": { @@ -2190,10 +2297,25 @@ "removeEventListener": true }, "packages": { - "@metamask/utils": true, + "@metamask/post-message-stream>@metamask/utils": true, "readable-stream": true } }, + "@metamask/post-message-stream>@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/ppom-validator": { "globals": { "URL": true, @@ -2321,24 +2443,9 @@ "packages": { "@metamask/base-controller": true, "@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/selected-network-controller": true - } - }, - "@metamask/queued-request-controller>@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/selected-network-controller": true, + "@metamask/utils": true } }, "@metamask/rate-limit-controller": { @@ -2397,23 +2504,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@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/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2459,16 +2551,42 @@ "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>jsonschema": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, "@metamask/utils": true, "browserify>buffer": true, "uuid": true, "webpack>events": true } }, + "@metamask/signature-controller>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/abi-utils": true, + "@metamask/eth-sig-util>tweetnacl": true, + "@metamask/signature-controller>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true + } + }, + "@metamask/signature-controller>@metamask/eth-sig-util>@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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2482,9 +2600,9 @@ "@ethersproject/bytes": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "@metamask/smart-transactions-controller>@metamask/polling-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller": true, "@metamask/smart-transactions-controller>bignumber.js": true, "browserify>buffer": true, @@ -2492,17 +2610,12 @@ "lodash": true } }, - "@metamask/smart-transactions-controller>@babel/runtime": { - "globals": { - "regeneratorRuntime": "write" - } - }, "@metamask/smart-transactions-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": true, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": true, - "@metamask/smart-transactions-controller>@ethereumjs/util": true + "@metamask/smart-transactions-controller>@ethereumjs/util": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true } }, "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/common": { @@ -2511,11 +2624,6 @@ "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/tx>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@ethereumjs/util": { "globals": { "console.warn": true, @@ -2523,15 +2631,10 @@ }, "packages": { "@ethereumjs/tx>ethereum-cryptography": true, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "webpack>events": true } }, - "@metamask/smart-transactions-controller>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, "@metamask/smart-transactions-controller>@metamask/base-controller": { "globals": { "setTimeout": true @@ -2545,6 +2648,18 @@ "crypto.getRandomValues": true } }, + "@metamask/smart-transactions-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/smart-transactions-controller>@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller": { "globals": { "clearTimeout": true, @@ -2569,9 +2684,9 @@ "@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, "browserify>buffer": true, + "eth-method-registry": true, "fast-json-patch": true, "lodash": true, "uuid": true, @@ -2581,21 +2696,26 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": { "packages": { "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": { "globals": { "console.warn": true }, "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, "@ethereumjs/tx>@ethereumjs/util>micro-ftch": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx>@ethereumjs/rlp": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, "webpack>events": true @@ -2620,10 +2740,10 @@ "@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/rpc-errors>@metamask/utils": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors>@metamask/utils": { "globals": { "TextDecoder": true, "TextEncoder": true @@ -2638,56 +2758,19 @@ "semver": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { - "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": { - "packages": { - "@metamask/ethjs>ethjs-abi": true, - "@metamask/ethjs>js-sha3": true, - "@metamask/smart-transactions-controller>@babel/runtime": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-filter": { - "globals": { - "clearInterval": true, - "setInterval": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": { - "packages": { - "@metamask/ethjs>@metamask/ethjs-util>is-hex-prefixed": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "browserify>buffer": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query": { + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { - "console": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": true, - "promise-to-callback": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-format": { - "packages": { - "@metamask/ethjs-query>@metamask/ethjs-format>ethjs-schema": true, - "@metamask/ethjs>@metamask/ethjs-util>strip-hex-prefix": true, - "@metamask/ethjs>@metamask/number-to-bn": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract>@metamask/ethjs-util": true - } - }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-query>@metamask/ethjs-rpc": { - "packages": { - "promise-to-callback": true + "@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/smart-transactions-controller>bignumber.js": { @@ -2706,59 +2789,34 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, + "@metamask/json-rpc-middleware-stream": true, "@metamask/object-multiplex": true, "@metamask/post-message-stream": 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/rpc-errors": 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, "@metamask/snaps-controllers>nanoid": true, "@metamask/snaps-controllers>readable-web-to-node-stream": true, - "@metamask/snaps-controllers>tar-stream": true, - "@metamask/snaps-rpc-methods": true, - "@metamask/snaps-sdk": true, - "@metamask/snaps-utils": true, - "@metamask/snaps-utils>@metamask/snaps-registry": true, - "@metamask/utils": true, - "browserify>browserify-zlib": true, - "eslint>fast-deep-equal": true, - "immer": true, - "readable-stream": true - } - }, - "@metamask/snaps-controllers-flask>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": { - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/snaps-controllers>tar-stream": true, + "@metamask/snaps-rpc-methods": true, + "@metamask/snaps-sdk": true, + "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, + "@metamask/utils": true, + "browserify>browserify-zlib": true, + "eslint>fast-deep-equal": true, + "immer": true, + "readable-stream": true, + "semver": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { + "@metamask/snaps-controllers-flask>nanoid": { "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true + "crypto.getRandomValues": true } }, "@metamask/snaps-controllers>@metamask/permission-controller": { @@ -2766,22 +2824,16 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, - "@metamask/snaps-controllers>@metamask/rpc-errors": true, + "@metamask/json-rpc-engine": true, + "@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, @@ -2839,8 +2891,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, @@ -2854,48 +2906,27 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": 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/json-rpc-engine": true, + "@metamask/rpc-errors": 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 } }, - "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "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/snaps-sdk>@metamask/rpc-errors": true, + "@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2904,15 +2935,24 @@ "packages": { "@metamask/message-signing-snap>@noble/curves": true, "@metamask/scure-bip39": true, - "@metamask/utils": true, + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": true, "@metamask/utils>@scure/base": true, "@noble/hashes": true } }, - "@metamask/snaps-sdk>@metamask/rpc-errors": { + "@metamask/snaps-sdk>@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@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/snaps-utils": { @@ -2931,10 +2971,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, @@ -2950,47 +2990,26 @@ "semver": true } }, - "@metamask/snaps-utils>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/snaps-utils>@metamask/permission-controller": { "globals": { "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/snaps-utils>@metamask/base-controller": true, - "@metamask/snaps-utils>@metamask/permission-controller>@metamask/json-rpc-engine": true, + "@metamask/json-rpc-engine": true, + "@metamask/rpc-errors": 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, @@ -3065,7 +3084,7 @@ "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/utils": true, + "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -3091,21 +3110,6 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@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": { "globals": { "fetch": true @@ -3114,9 +3118,9 @@ "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, - "@metamask/gas-fee-controller>@metamask/polling-controller": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/polling-controller": true, "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, @@ -3134,6 +3138,18 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/user-operation-controller>@metamask/base-controller": true, + "uuid": true + } + }, "@metamask/user-operation-controller>@metamask/rpc-errors": { "packages": { "@metamask/rpc-errors>fast-safe-stringify": true, @@ -3449,6 +3465,51 @@ "process": true } }, + "@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@solana/addresses>@solana/assertions": true, + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/codecs-strings": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/assertions": { + "globals": { + "crypto": true, + "isSecureContext": true + }, + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-core": { + "packages": { + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/codecs-strings": { + "globals": { + "TextDecoder": true, + "TextEncoder": true, + "atob": true, + "btoa": true + }, + "packages": { + "@solana/addresses>@solana/codecs-core": true, + "@solana/addresses>@solana/errors": true + } + }, + "@solana/addresses>@solana/errors": { + "globals": { + "btoa": true + } + }, "@storybook/addon-docs>remark-external-links>mdast-util-definitions": { "packages": { "react-markdown>unist-util-visit": true @@ -4169,59 +4230,16 @@ "setInterval": true }, "packages": { + "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "bn.js": true, "browserify>buffer": true, "crypto-browserify": true, - "eth-lattice-keyring>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk": true, "eth-lattice-keyring>rlp": true, "webpack>events": true } }, - "eth-lattice-keyring>@ethereumjs/tx": { - "packages": { - "@ethereumjs/tx>@ethereumjs/common": true, - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": { - "packages": { - "browserify": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>case": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { - "globals": { - "WeakRef": true - }, - "packages": { - "browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "eth-lattice-keyring>gridplus-sdk": { "globals": { "AbortController": true, @@ -4239,67 +4257,59 @@ "packages": { "@ethereumjs/tx>@ethereumjs/common>crc-32": true, "@ethersproject/abi": true, + "@metamask/eth-sig-util": true, "@metamask/ethjs>js-sha3": true, "@metamask/keyring-api>bech32": true, + "@metamask/ppom-validator>elliptic": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true, "eth-lattice-keyring>gridplus-sdk>aes-js": true, "eth-lattice-keyring>gridplus-sdk>bignumber.js": true, - "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, - "eth-lattice-keyring>gridplus-sdk>elliptic": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, - "eth-lattice-keyring>gridplus-sdk>rlp": true, + "eth-lattice-keyring>gridplus-sdk>bs58check": true, + "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethers>@ethersproject/sha2>hash.js": true, - "ganache>secp256k1": true, "lodash": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": { "packages": { - "@ethereumjs/tx>@ethereumjs/rlp": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "@ethersproject/providers": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/tx>@chainsafe/ssz": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": true + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true } }, "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": { "packages": { - "@ethereumjs/tx>@ethereumjs/common>crc-32": true, - "@ethereumjs/tx>@ethereumjs/util": true, - "browserify>buffer": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": true, "webpack>events": true } }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography": { + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/util": { "globals": { - "TextDecoder": true, - "crypto": true + "console.warn": true, + "fetch": true }, "packages": { - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true + "@ethereumjs/tx>ethereum-cryptography": true, + "eth-lattice-keyring>gridplus-sdk>@ethereumjs/rlp": true, + "webpack>events": true } }, "eth-lattice-keyring>gridplus-sdk>aes-js": { @@ -4313,11 +4323,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4339,33 +4344,24 @@ "globals": { "URL": true, "URLSearchParams": true, - "location": true + "location": true, + "navigator": true } }, - "eth-lattice-keyring>gridplus-sdk>elliptic": { + "eth-lattice-keyring>gridplus-sdk>bs58check": { "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "@noble/hashes": true, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": true } }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": { - "globals": { - "intToBuffer": true - }, + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58": { "packages": { - "@metamask/ethjs>js-sha3": true, - "bn.js": true, - "buffer": true + "eth-lattice-keyring>gridplus-sdk>bs58check>bs58>base-x": true } }, - "eth-lattice-keyring>gridplus-sdk>rlp": { - "globals": { - "TextEncoder": true + "eth-lattice-keyring>gridplus-sdk>secp256k1": { + "packages": { + "@metamask/ppom-validator>elliptic": true } }, "eth-lattice-keyring>gridplus-sdk>uuid": { @@ -4689,20 +4685,9 @@ "ethers>@ethersproject/signing-key": { "packages": { "@ethersproject/bytes": true, + "@metamask/ppom-validator>elliptic": true, "ethers>@ethersproject/logger": true, - "ethers>@ethersproject/properties": true, - "ethers>@ethersproject/signing-key>elliptic": true - } - }, - "ethers>@ethersproject/signing-key>elliptic": { - "packages": { - "@metamask/ppom-validator>elliptic>brorand": true, - "@metamask/ppom-validator>elliptic>hmac-drbg": true, - "@metamask/ppom-validator>elliptic>minimalistic-assert": true, - "@metamask/ppom-validator>elliptic>minimalistic-crypto-utils": true, - "bn.js": true, - "ethers>@ethersproject/sha2>hash.js": true, - "pumpify>inherits": true + "ethers>@ethersproject/properties": true } }, "ethers>@ethersproject/solidity": { @@ -4835,16 +4820,6 @@ "stream-http": true } }, - "json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "readable-stream": true - } - }, "koa>content-disposition>safe-buffer": { "packages": { "browserify>buffer": true @@ -5361,10 +5336,10 @@ "document": true }, "packages": { + "@babel/runtime": true, "prop-types": true, "react": true, "react-dom": true, - "react-redux>@babel/runtime": true, "react-redux>hoist-non-react-statics": true, "react-redux>react-is": true } @@ -5564,16 +5539,6 @@ "webpack>events": true } }, - "readable-stream-2>core-util-is": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "readable-stream-2>process-nextick-args": { - "packages": { - "process": true - } - }, "readable-stream>util-deprecate": { "globals": { "console.trace": true, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index e7ce64ceec23..2d6b7ce3ad14 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -64,6 +64,7 @@ "v8": true }, "globals": { + "URL": true, "console.error": true, "console.log": true, "process.env.BABEL_ENV": true, @@ -123,12 +124,14 @@ }, "@babel/core>@babel/generator>jsesc": { "globals": { - "Buffer.isBuffer": true + "Buffer": true } }, "@babel/core>@babel/helper-compilation-targets": { "globals": { "console.warn": true, + "process.env.BROWSERSLIST": true, + "process.env.BROWSERSLIST_CONFIG": true, "process.versions.node": true }, "packages": { @@ -180,8 +183,7 @@ "@babel/core>@babel/helpers": { "packages": { "@babel/core>@babel/template": true, - "@babel/core>@babel/types": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/types": true } }, "@babel/core>@babel/template": { @@ -194,11 +196,10 @@ "@babel/core>@babel/types": { "globals": { "console.warn": true, - "process.env.BABEL_TYPES_8_BREAKING": true + "process.env": true }, "packages": { "@babel/core>@babel/types>@babel/helper-string-parser": true, - "@babel/core>@babel/types>to-fast-properties": true, "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true } }, @@ -282,30 +283,17 @@ }, "packages": { "@babel/core>@babel/helper-compilation-targets": true, - "@babel/core>@babel/types": true, "@babel/plugin-transform-logical-assignment-operators": true, "@babel/preset-env>@babel/compat-data": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/helper-validator-option": true, + "@babel/preset-env>@babel/plugin-bugfix-firefox-class-in-computed-class-key": true, + "@babel/preset-env>@babel/plugin-bugfix-safari-class-field-initializer-scope": true, "@babel/preset-env>@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": true, "@babel/preset-env>@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": true, - "@babel/preset-env>@babel/plugin-syntax-async-generators": true, - "@babel/preset-env>@babel/plugin-syntax-class-properties": true, - "@babel/preset-env>@babel/plugin-syntax-class-static-block": true, - "@babel/preset-env>@babel/plugin-syntax-dynamic-import": true, - "@babel/preset-env>@babel/plugin-syntax-export-namespace-from": true, + "@babel/preset-env>@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": true, "@babel/preset-env>@babel/plugin-syntax-import-assertions": true, "@babel/preset-env>@babel/plugin-syntax-import-attributes": true, - "@babel/preset-env>@babel/plugin-syntax-import-meta": true, - "@babel/preset-env>@babel/plugin-syntax-json-strings": true, - "@babel/preset-env>@babel/plugin-syntax-logical-assignment-operators": true, - "@babel/preset-env>@babel/plugin-syntax-nullish-coalescing-operator": true, - "@babel/preset-env>@babel/plugin-syntax-numeric-separator": true, - "@babel/preset-env>@babel/plugin-syntax-object-rest-spread": true, - "@babel/preset-env>@babel/plugin-syntax-optional-catch-binding": true, - "@babel/preset-env>@babel/plugin-syntax-optional-chaining": true, - "@babel/preset-env>@babel/plugin-syntax-private-property-in-object": true, - "@babel/preset-env>@babel/plugin-syntax-top-level-await": true, "@babel/preset-env>@babel/plugin-syntax-unicode-sets-regex": true, "@babel/preset-env>@babel/plugin-transform-arrow-functions": true, "@babel/preset-env>@babel/plugin-transform-async-generator-functions": true, @@ -319,6 +307,7 @@ "@babel/preset-env>@babel/plugin-transform-destructuring": true, "@babel/preset-env>@babel/plugin-transform-dotall-regex": true, "@babel/preset-env>@babel/plugin-transform-duplicate-keys": true, + "@babel/preset-env>@babel/plugin-transform-duplicate-named-capturing-groups-regex": true, "@babel/preset-env>@babel/plugin-transform-dynamic-import": true, "@babel/preset-env>@babel/plugin-transform-exponentiation-operator": true, "@babel/preset-env>@babel/plugin-transform-export-namespace-from": true, @@ -362,42 +351,36 @@ "@babel/preset-env>semver": true } }, - "@babel/preset-env>@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "@babel/preset-env>@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "packages": { - "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-transform-optional-chaining": true, - "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-async-generators": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true + "depcheck>@babel/traverse": true } }, - "@babel/preset-env>@babel/plugin-syntax-class-properties": { + "@babel/preset-env>@babel/plugin-bugfix-safari-class-field-initializer-scope": { "packages": { + "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true } }, - "@babel/preset-env>@babel/plugin-syntax-class-static-block": { + "@babel/preset-env>@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true } }, - "@babel/preset-env>@babel/plugin-syntax-dynamic-import": { + "@babel/preset-env>@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true + "@babel/core": true, + "@babel/preset-env>@babel/helper-plugin-utils": true, + "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true, + "@babel/preset-env>@babel/plugin-transform-optional-chaining": true } }, - "@babel/preset-env>@babel/plugin-syntax-export-namespace-from": { + "@babel/preset-env>@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true + "@babel/core": true, + "@babel/preset-env>@babel/helper-plugin-utils": true, + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-syntax-import-assertions": { @@ -410,56 +393,6 @@ "@babel/preset-env>@babel/helper-plugin-utils": true } }, - "@babel/preset-env>@babel/plugin-syntax-import-meta": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-json-strings": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-logical-assignment-operators": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-nullish-coalescing-operator": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-numeric-separator": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-object-rest-spread": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-optional-catch-binding": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-optional-chaining": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-private-property-in-object": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-syntax-top-level-await": { - "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, "@babel/preset-env>@babel/plugin-syntax-unicode-sets-regex": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, @@ -475,9 +408,8 @@ "packages": { "@babel/core": true, "@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, - "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator": { @@ -493,14 +425,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, - "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-environment-visitor": true + "depcheck>@babel/traverse": 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, - "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-block-scoped-functions": { @@ -524,7 +456,6 @@ "@babel/preset-env>@babel/plugin-transform-class-static-block": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-class-static-block": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin": true } }, @@ -534,12 +465,9 @@ "@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>@babel/helper-split-export-declaration": true, - "@babel/preset-env>@babel/plugin-transform-classes>globals": true + "@babel/preset-env>@babel/plugin-transform-classes>globals": true, + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": { @@ -547,22 +475,11 @@ "@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 - } - }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": { "packages": { "@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, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": true, "depcheck>@babel/traverse": true } }, @@ -571,7 +488,7 @@ "@babel/core>@babel/types": true } }, - "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-split-export-declaration": { + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": { "packages": { "@babel/core>@babel/types": true } @@ -608,19 +525,19 @@ "characterClassItem.kind": true }, "packages": { - "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>@babel/regjsgen": true, "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>regenerate": true, + "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>regjsgen": true, "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>regjsparser": true, "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>unicode-match-property-ecmascript": true, "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>unicode-match-property-value-ecmascript": true } }, - "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>@babel/regjsgen": { + "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>regenerate": { "globals": { "define": true } }, - "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>regenerate": { + "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin>regexpu-core>regjsgen": { "globals": { "define": true } @@ -648,10 +565,15 @@ "@babel/preset-env>@babel/helper-plugin-utils": true } }, - "@babel/preset-env>@babel/plugin-transform-dynamic-import": { + "@babel/preset-env>@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-dynamic-import": true + "@babel/preset-env>@babel/plugin-transform-dotall-regex>@babel/helper-create-regexp-features-plugin": true + } + }, + "@babel/preset-env>@babel/plugin-transform-dynamic-import": { + "packages": { + "@babel/preset-env>@babel/helper-plugin-utils": true } }, "@babel/preset-env>@babel/plugin-transform-exponentiation-operator": { @@ -669,27 +591,31 @@ "@babel/preset-env>@babel/plugin-transform-export-namespace-from": { "packages": { "@babel/core": true, - "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-export-namespace-from": true + "@babel/preset-env>@babel/helper-plugin-utils": true } }, "@babel/preset-env>@babel/plugin-transform-for-of": { "packages": { "@babel/core": true, - "@babel/preset-env>@babel/helper-plugin-utils": true + "@babel/preset-env>@babel/helper-plugin-utils": true, + "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true + } + }, + "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": { + "packages": { + "@babel/core>@babel/types": true } }, "@babel/preset-env>@babel/plugin-transform-function-name": { "packages": { "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-function-name": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-json-strings": { "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-json-strings": true + "@babel/preset-env>@babel/helper-plugin-utils": true } }, "@babel/preset-env>@babel/plugin-transform-literals": { @@ -726,15 +652,10 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-transform-modules-systemjs>@babel/helper-hoist-variables": true, + "depcheck>@babel/traverse": 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, @@ -761,23 +682,19 @@ "@babel/preset-env>@babel/plugin-transform-nullish-coalescing-operator": { "packages": { "@babel/core": true, - "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-nullish-coalescing-operator": true + "@babel/preset-env>@babel/helper-plugin-utils": true } }, "@babel/preset-env>@babel/plugin-transform-numeric-separator": { "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-numeric-separator": true + "@babel/preset-env>@babel/helper-plugin-utils": true } }, "@babel/preset-env>@babel/plugin-transform-object-rest-spread": { "packages": { "@babel/core": true, "@babel/core>@babel/helper-compilation-targets": true, - "@babel/preset-env>@babel/compat-data": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-object-rest-spread": true, "@babel/preset-env>@babel/plugin-transform-parameters": true } }, @@ -790,16 +707,14 @@ }, "@babel/preset-env>@babel/plugin-transform-optional-catch-binding": { "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-optional-catch-binding": true + "@babel/preset-env>@babel/helper-plugin-utils": true } }, "@babel/preset-env>@babel/plugin-transform-optional-chaining": { "packages": { "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-optional-chaining": true, - "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true + "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true } }, "@babel/preset-env>@babel/plugin-transform-parameters": { @@ -821,11 +736,11 @@ "packages": { "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": 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>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, + "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": true, + "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": 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": true } }, @@ -838,7 +753,6 @@ "@babel/preset-env>@babel/plugin-transform-private-property-in-object": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-syntax-private-property-in-object": 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 } @@ -880,12 +794,7 @@ "packages": { "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": true - } - }, - "@babel/preset-env>@babel/plugin-transform-spread>@babel/helper-skip-transparent-expression-wrappers": { - "packages": { - "@babel/core>@babel/types": true + "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true } }, "@babel/preset-env>@babel/plugin-transform-sticky-regex": { @@ -1054,8 +963,8 @@ "@babel/core": 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-for-of>@babel/helper-skip-transparent-expression-wrappers": 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 } }, @@ -3095,7 +3004,7 @@ "eslint-plugin-prettier": true, "eslint-plugin-react": true, "eslint-plugin-react-hooks": true, - "eslint>@eslint/eslintrc>ajv": true, + "eslint>ajv": true, "eslint>globals": true, "eslint>ignore": true, "eslint>minimatch": true, @@ -3103,17 +3012,6 @@ "nock>debug": true } }, - "eslint>@eslint/eslintrc>ajv": { - "globals": { - "console": true - }, - "packages": { - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "eslint>@eslint/eslintrc>ajv>json-schema-traverse": true, - "eslint>fast-deep-equal": true, - "uri-js": true - } - }, "eslint>@eslint/eslintrc>import-fresh": { "builtin": { "path.dirname": true @@ -6328,24 +6226,23 @@ "packages": { "@babel/code-frame": true, "@babel/core>@babel/generator": 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, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-environment-visitor": true, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name": true, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables": true, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration": true, "nock>debug": true } }, "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/traverse>@babel/types": { "globals": { "console.warn": true, - "process.env.BABEL_TYPES_8_BREAKING": true + "process.env": true }, "packages": { "@babel/core>@babel/types>@babel/helper-string-parser": true, - "@babel/core>@babel/types>to-fast-properties": true, "lavamoat>@babel/highlight>@babel/helper-validator-identifier": true } }, @@ -6373,9 +6270,9 @@ "packages": { "@babel/register>clone-deep>is-plain-object": true, "lavamoat>lavamoat-core>merge-deep>clone-deep>for-own": true, - "lavamoat>lavamoat-core>merge-deep>clone-deep>kind-of": true, "lavamoat>lavamoat-core>merge-deep>clone-deep>lazy-cache": true, - "lavamoat>lavamoat-core>merge-deep>clone-deep>shallow-clone": true + "lavamoat>lavamoat-core>merge-deep>clone-deep>shallow-clone": true, + "lavamoat>lavamoat-core>merge-deep>kind-of": true } }, "lavamoat>lavamoat-core>merge-deep>clone-deep>for-own": { @@ -6383,11 +6280,6 @@ "gulp>undertaker>object.reduce>for-own>for-in": true } }, - "lavamoat>lavamoat-core>merge-deep>clone-deep>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, "lavamoat>lavamoat-core>merge-deep>clone-deep>lazy-cache": { "globals": { "process.env.TRAVIS": true, @@ -6426,6 +6318,22 @@ "browserify>insert-module-globals>is-buffer": true } }, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-function-name": { + "packages": { + "@babel/core>@babel/template": true, + "@babel/core>@babel/types": true + } + }, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-hoist-variables": { + "packages": { + "@babel/core>@babel/types": true + } + }, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/helper-split-export-declaration": { + "packages": { + "@babel/core>@babel/types": true + } + }, "lodash": { "globals": { "define": true @@ -6686,13 +6594,8 @@ } }, "postcss>picocolors": { - "builtin": { - "tty.isatty": true - }, "globals": { - "process.argv.includes": true, - "process.env": true, - "process.platform": true + "process": true } }, "postcss>source-map-js": { @@ -8577,17 +8480,12 @@ "process.stdout.write": true }, "packages": { + "eslint>ajv": true, "lodash": true, - "stylelint>table>ajv": true, "stylelint>table>slice-ansi": true, "stylelint>table>string-width": true } }, - "stylelint>table>ajv": { - "packages": { - "eslint>fast-deep-equal": true - } - }, "stylelint>table>slice-ansi": { "packages": { "stylelint>table>slice-ansi>ansi-styles": true, diff --git a/package.json b/package.json index 1a217ec7bd9a..15df8313bf90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.6.0", + "version": "12.6.1", "private": true, "repository": { "type": "git", @@ -48,8 +48,8 @@ "test:unit:coverage": "jest --coverage", "test:unit:webpack": "tsx --test development/webpack/test/*.test.ts", "test:unit:webpack:coverage": "nyc --reporter=html --reporter=json --reporter=text --report-dir=./coverage/webpack tsx --test development/webpack/test/*.test.ts", - "test:integration": "jest --config jest.integration.config.js", - "test:integration:coverage": "jest --config jest.integration.config.js --coverage", + "test:integration": "npx webpack build --config ./development/webpack/webpack.integration.tests.config.ts && jest --config jest.integration.config.js", + "test:integration:coverage": "yarn test:integration --coverage", "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", @@ -132,8 +132,12 @@ }, "resolutions": { "chokidar": "^3.6.0", + "gridplus-sdk/elliptic": "^6.5.7", + "gridplus-sdk/secp256k1": "^5.0.1", + "eth-lattice-keyring/@ethereumjs/tx": "^4.2.0", + "@ethersproject/signing-key/elliptic": "^6.5.7", + "ganache/secp256k1": "^4.0.4", "simple-update-notifier@^1.0.0": "^2.0.0", - "@babel/core": "patch:@babel/core@npm%3A7.23.2#~/.yarn/patches/@babel-core-npm-7.23.2-b93f586907.patch", "@types/react": "^16.9.53", "analytics-node/axios": "^0.21.2", "bn.js": "^5.2.1", @@ -229,28 +233,10 @@ "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.9.0", + "@metamask/snaps-sdk": "^6.10.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", - "@babel/runtime@npm:^7.12.5": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.10.3": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.5.5": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.3.1": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.0.0": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.1.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.12.13": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.4.4": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.10.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.21.0": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.17.8": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.13.10": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.12.0": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.8.7": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.4.0": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.18.3": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.8.3": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@babel/runtime@npm:^7.8.4": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", + "@babel/core": "patch:@babel/core@npm%3A7.25.9#~/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch", + "@babel/runtime": "patch:@babel/runtime@npm%3A7.25.9#~/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.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", "ts-mixer@npm:^6.0.3": "patch:ts-mixer@npm%3A6.0.4#~/.yarn/patches/ts-mixer-npm-6.0.4-5d9747bdf5.patch", @@ -264,14 +250,10 @@ "@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", - "secp256k1@npm:^4.0.0": "4.0.4", - "secp256k1@npm:^4.0.1": "4.0.4", - "secp256k1@npm:4.0.2": "4.0.4", - "secp256k1@npm:4.0.3": "4.0.4" + "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", + "@babel/runtime": "patch:@babel/runtime@npm%3A7.25.9#~/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch", "@blockaid/ppom_release": "^1.5.3", "@ensdomains/content-hash": "^2.5.7", "@ethereumjs/tx": "^4.1.1", @@ -285,12 +267,12 @@ "@ethersproject/wallet": "^5.7.0", "@fortawesome/fontawesome-free": "^5.13.0", "@keystonehq/bc-ur-registry-eth": "^0.19.1", - "@keystonehq/metamask-airgapped-keyring": "^0.13.1", + "@keystonehq/metamask-airgapped-keyring": "^0.14.1", "@lavamoat/lavadome-react": "0.0.17", "@lavamoat/snow": "^2.0.2", "@material-ui/core": "^4.11.0", "@metamask-institutional/custody-controller": "^0.3.0", - "@metamask-institutional/custody-keyring": "^2.1.0", + "@metamask-institutional/custody-keyring": "^2.1.1", "@metamask-institutional/extension": "^0.3.28", "@metamask-institutional/institutional-features": "^1.3.6", "@metamask-institutional/portfolio-dashboard": "^1.4.1", @@ -304,16 +286,16 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", - "@metamask/controller-utils": "^11.2.0", + "@metamask/controller-utils": "^11.4.0", "@metamask/design-tokens": "^4.0.0", "@metamask/ens-controller": "^13.0.0", "@metamask/ens-resolver-snap": "^0.1.2", - "@metamask/eth-json-rpc-filters": "^7.0.0", + "@metamask/eth-json-rpc-filters": "^9.0.0", "@metamask/eth-json-rpc-middleware": "patch:@metamask/eth-json-rpc-middleware@npm%3A14.0.1#~/.yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch", "@metamask/eth-ledger-bridge-keyring": "^3.0.1", "@metamask/eth-query": "^4.0.0", @@ -328,6 +310,7 @@ "@metamask/gas-fee-controller": "^18.0.0", "@metamask/jazzicon": "^2.0.0", "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/json-rpc-middleware-stream": "^8.0.4", "@metamask/keyring-api": "^8.1.3", "@metamask/keyring-controller": "^17.2.2", "@metamask/logging-controller": "^6.0.0", @@ -338,33 +321,35 @@ "@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", "@metamask/notification-controller": "^6.0.0", - "@metamask/notification-services-controller": "^0.7.0", + "@metamask/notification-services-controller": "^0.11.0", "@metamask/object-multiplex": "^2.0.0", "@metamask/obs-store": "^9.0.0", "@metamask/permission-controller": "^10.0.0", "@metamask/permission-log-controller": "^2.0.1", "@metamask/phishing-controller": "^12.3.0", + "@metamask/polling-controller": "^10.0.1", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.35.1", "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", - "@metamask/queued-request-controller": "^7.0.0", + "@metamask/queued-request-controller": "^7.0.1", "@metamask/rate-limit-controller": "^6.0.0", "@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.2", - "@metamask/signature-controller": "^20.0.0", + "@metamask/signature-controller": "^21.0.0", "@metamask/smart-transactions-controller": "^13.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": "^38.1.0", + "@metamask/snaps-controllers": "^9.12.0", + "@metamask/snaps-execution-environments": "^6.9.2", + "@metamask/snaps-rpc-methods": "^11.5.1", + "@metamask/snaps-sdk": "^6.10.0", + "@metamask/snaps-utils": "^8.5.1", + "@metamask/solana-wallet-snap": "^0.1.9", + "@metamask/transaction-controller": "^38.3.0", "@metamask/user-operation-controller": "^13.0.0", - "@metamask/utils": "^9.3.0", + "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.4.0", @@ -373,6 +358,7 @@ "@sentry/browser": "^8.33.1", "@sentry/types": "^8.33.1", "@sentry/utils": "^8.33.1", + "@solana/addresses": "2.0.0-rc.4", "@swc/core": "1.4.11", "@trezor/connect-web": "^9.4.0", "@zxing/browser": "^0.1.4", @@ -406,7 +392,6 @@ "immer": "^9.0.6", "is-retry-allowed": "^2.2.0", "jest-junit": "^14.0.1", - "json-rpc-middleware-stream": "^5.0.1", "labeled-stream-splicer": "^2.0.2", "localforage": "^1.9.0", "lodash": "^4.17.21", @@ -455,15 +440,15 @@ "devDependencies": { "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", - "@babel/code-frame": "^7.22.13", - "@babel/core": "^7.23.2", - "@babel/eslint-parser": "^7.23.10", - "@babel/eslint-plugin": "^7.23.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/preset-env": "^7.23.2", - "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.23.2", - "@babel/register": "^7.22.15", + "@babel/code-frame": "^7.25.9", + "@babel/core": "patch:@babel/core@npm%3A7.25.9#~/.yarn/patches/@babel-core-npm-7.25.9-4ae3bff7f3.patch", + "@babel/eslint-parser": "^7.25.9", + "@babel/eslint-plugin": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/register": "^7.25.9", "@jest/globals": "^29.7.0", "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/lavadome-core": "0.0.10", @@ -480,10 +465,10 @@ "@metamask/eslint-config-typescript": "^9.0.1", "@metamask/eslint-plugin-design-tokens": "^1.1.0", "@metamask/forwarder": "^1.1.0", - "@metamask/phishing-warning": "^4.0.0", + "@metamask/phishing-warning": "^4.1.0", "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", - "@metamask/test-dapp": "8.7.0", + "@metamask/test-dapp": "8.13.0", "@octokit/core": "^3.6.0", "@open-rpc/meta-schema": "^1.14.6", "@open-rpc/mock-server": "^1.7.5", @@ -543,6 +528,7 @@ "@types/redux-mock-store": "1.0.6", "@types/remote-redux-devtools": "^0.5.5", "@types/selenium-webdriver": "^4.1.19", + "@types/serve-handler": "^6.1.4", "@types/sinon": "^10.0.13", "@types/sprintf-js": "^1", "@types/w3c-web-hid": "^1.0.3", @@ -612,13 +598,14 @@ "gulp-stylelint": "^13.0.0", "gulp-watch": "^5.0.1", "gulp-zip": "^5.1.0", - "html-bundler-webpack-plugin": "^3.17.3", + "html-bundler-webpack-plugin": "^4.4.1", "https-browserify": "^1.0.0", "husky": "^8.0.3", "ini": "^3.0.0", "jest": "^29.7.0", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "patch:jest-environment-jsdom@npm%3A29.7.0#~/.yarn/patches/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b.patch", + "jest-preview": "^0.3.1", "jsdom": "^16.7.0", "json-schema-to-ts": "^3.0.1", "koa": "^2.7.0", @@ -628,6 +615,7 @@ "level": "^8.0.1", "lockfile-lint": "^4.10.6", "loose-envify": "^1.4.0", + "mini-css-extract-plugin": "^2.9.1", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mockttp": "^3.10.1", @@ -684,7 +672,8 @@ "wait-on": "^7.0.1", "watchify": "^4.0.0", "webextension-polyfill": "^0.8.0", - "webpack": "^5.91.0", + "webpack": "^5.96.1", + "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.3", "ws": "^8.17.1", "yaml": "^2.4.1", @@ -702,17 +691,10 @@ "@eth-optimism/contracts>@ethersproject/hardware-wallets>@ledgerhq/hw-transport-node-hid>@ledgerhq/hw-transport-node-hid-noevents>node-hid": false, "@eth-optimism/contracts>@ethersproject/hardware-wallets>@ledgerhq/hw-transport-node-hid>node-hid": false, "@eth-optimism/contracts>@ethersproject/hardware-wallets>@ledgerhq/hw-transport-node-hid>usb": false, - "@metamask/controllers>web3-provider-engine>ethereumjs-util>keccak": false, - "@metamask/controllers>web3-provider-engine>ethereumjs-util>secp256k1": false, - "@metamask/controllers>web3-provider-engine>ethereumjs-vm>merkle-patricia-tree>ethereumjs-util>keccak": false, - "@metamask/controllers>web3-provider-engine>ethereumjs-vm>merkle-patricia-tree>ethereumjs-util>secp256k1": false, - "@metamask/eth-ledger-bridge-keyring>hdkey>secp256k1": false, "@storybook/api>core-js": false, "@storybook/core>@storybook/core-client>@storybook/ui>core-js-pure": false, "@storybook/test-runner>@storybook/core-common>esbuild": false, - "eth-json-rpc-filters>eth-json-rpc-middleware>ethereumjs-util>keccak": false, - "eth-json-rpc-filters>eth-json-rpc-middleware>ethereumjs-util>secp256k1": false, - "eth-lattice-keyring>gridplus-sdk": false, + "eth-lattice-keyring>gridplus-sdk": true, "ethereumjs-util>ethereum-cryptography>keccak": false, "ganache>@trufflesuite/bigint-buffer": false, "ganache>@trufflesuite/uws-js-unofficial>bufferutil": false, @@ -722,13 +704,10 @@ "ganache>leveldown": false, "ganache>secp256k1": false, "ganache>utf-8-validate": false, - "ethereumjs-util>ethereum-cryptography>secp256k1": false, "gulp-watch>chokidar>fsevents": false, "gulp>glob-watcher>chokidar>fsevents": false, "webpack>watchpack>watchpack-chokidar2>chokidar>fsevents": false, - "@keystonehq/bc-ur-registry-eth>hdkey>secp256k1": false, "eth-lattice-keyring>gridplus-sdk>secp256k1": false, - "eth-lattice-keyring>secp256k1": false, "@storybook/react>@pmmmwh/react-refresh-webpack-plugin>core-js-pure": false, "@testing-library/jest-dom>aria-query>@babel/runtime-corejs3>core-js-pure": false, "web3": false, @@ -737,7 +716,6 @@ "web3>web3-core>web3-core-requestmanager>web3-providers-ws>websocket>es5-ext": false, "web3>web3-core>web3-core-requestmanager>web3-providers-ws>websocket>utf-8-validate": false, "web3>web3-shh": false, - "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>hdkey>secp256k1": false, "@metamask/base-controller>simple-git-hooks": false, "@storybook/core>@storybook/core-server>webpack>watchpack>watchpack-chokidar2>chokidar>fsevents": false, "resolve-url-loader>es6-iterator>es5-ext": false, @@ -769,8 +747,9 @@ "core-js-pure": true, "resolve-url-loader>es6-iterator>d>es5-ext": false, "resolve-url-loader>es6-iterator>d>es5-ext>esniff>es5-ext": false, - "level>classic-level": false + "level>classic-level": false, + "jest-preview": false } }, - "packageManager": "yarn@4.4.1" + "packageManager": "yarn@4.5.1" } diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 41b04a9b5210..817d1e102bff 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -32,6 +32,7 @@ "localhost:8000", "localhost:8545", "mainnet.infura.io", + "metamask-sdk.api.cx.metamask.io", "metamask.eth", "metamask.github.io", "metametrics.metamask.test", @@ -48,11 +49,15 @@ "raw.githubusercontent.com", "registry.npmjs.org", "responsive-rpc.test", + "security-alerts.api.cx.metamask.io", + "security-alerts.dev-api.cx.metamask.io", "sentry.io", "snaps.metamask.io", "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", + "support.metamask.io", + "support.metamask-institutional.io", "swap.api.cx.metamask.io", "test.metamask-phishing.io", "token.api.cx.metamask.io", diff --git a/shared/constants/common.ts b/shared/constants/common.ts index f45ec8abd7e4..96d2b0c65b55 100644 --- a/shared/constants/common.ts +++ b/shared/constants/common.ts @@ -1,5 +1,59 @@ +import { CHAIN_IDS } from './network'; + export enum EtherDenomination { ETH = 'ETH', GWEI = 'GWEI', WEI = 'WEI', } + +const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; +const BSC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'BscScan'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Etherscan'; +const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/'; +const GOERLI_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Goerli Etherscan'; +const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; +const POLYGON_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'PolygonScan'; +const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; +const AVALANCHE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Snowtrace'; +const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/'; +const OPTIMISM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Optimism Explorer'; +const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/'; +const ARBITRUM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'ArbiScan'; +const ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL = 'https://explorer.zksync.io/'; +const ZKSYNC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Zksync Explorer'; +const LINEA_DEFAULT_BLOCK_EXPLORER_URL = 'https://lineascan.build/'; +const LINEA_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'LineaScan'; +const BASE_DEFAULT_BLOCK_EXPLORER_URL = 'https://basescan.org/'; +const BASE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'BaseScan'; + +type BlockExplorerUrlMap = { + [key: string]: string; +}; + +export const CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP: BlockExplorerUrlMap = { + [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_URL, +} as const; + +export const CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP: BlockExplorerUrlMap = + { + [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + } as const; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 8107a1040127..103a24c2463d 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -39,7 +39,7 @@ export type MetaMetricsReferrerObject = { * function, but still provides the consumer a way to override these values if * necessary. */ -type MetaMetricsContext = { +export type MetaMetricsContext = { /** * Application metadata. */ @@ -65,6 +65,10 @@ type MetaMetricsContext = { * The dapp that triggered an interaction (MetaMask only). */ referrer?: MetaMetricsReferrerObject; + /** + * The marketing campaign cookie ID. + */ + marketingCampaignCookieId?: string | null; }; export type MetaMetricsEventPayload = { @@ -79,7 +83,7 @@ export type MetaMetricsEventPayload = { /** * The action ID to deduplicate event requests from the UI. */ - actionId?: number; + actionId?: string; /** * The type of environment this event occurred in. Defaults to the background * process type. @@ -116,6 +120,14 @@ export type MetaMetricsEventPayload = { * The origin of the dapp that triggered this event. */ referrer?: MetaMetricsReferrerObject; + /* + * The unique identifier for the event. + */ + uniqueIdentifier?: string; + /** + * Whether the event is a duplicate of an anonymized event. + */ + isDuplicateAnonymizedEvent?: boolean; }; export type MetaMetricsEventOptions = { @@ -146,6 +158,10 @@ export type MetaMetricsEventOptions = { * as not conforming to our schema. */ matomoEvent?: boolean; + /** + * Values that can used in the "properties" tracking object as keys, + */ + contextPropsIntoEventProperties?: string | string[]; }; export type MetaMetricsEventFragment = { @@ -223,6 +239,25 @@ export type MetaMetricsEventFragment = { * to avoid unnecessary lookups and reduce accidental duplication. */ uniqueIdentifier?: string; + /* + * The event id. + */ + id: string; + /* + * The environment type. + */ + environmentType?: string; + /* + * The event name. + */ + event?: string; + + /** + * HACK: "transaction-submitted-" fragment hack + * If this is true and the fragment is found as an abandoned fragment, + * then delete the fragment instead of finalizing it. + */ + canDeleteIfAbandoned?: boolean; }; /** @@ -245,11 +280,38 @@ export type SegmentEventPayload = { /** * Properties to attach to the event. */ - properties: object; + properties: { + params?: Record; + legacy_event?: boolean; + locale: string; + chain_id: string; + environment_type?: string; + revenue?: number; + value?: number; + currency?: string; + category?: string; + }; /** * The context the event occurred in. */ context: MetaMetricsContext; + /** + * The message id + */ + messageId?: string; + + /** + * The timestamp of the event. + */ + timestamp?: string; + /* + * The event name. + */ + name?: string; + /* + * The user trais + */ + traits?: MetaMetricsUserTraits; }; /** @@ -259,18 +321,18 @@ export type MetaMetricsPagePayload = { /** * The name of the page that was viewed. */ - name: string; + name?: string; /** * The variadic parts of the page URL. * * Example: If the route is `/asset/:asset` and the path is `/asset/ETH`, * the `params` property would be `{ asset: 'ETH' }`. */ - params?: object; + params?: Record; /** * The environment type that the page was viewed in. */ - environmentType: EnvironmentType; + environmentType?: EnvironmentType; /** * The details of the page. */ @@ -279,6 +341,10 @@ export type MetaMetricsPagePayload = { * The dapp that triggered the page view. */ referrer?: MetaMetricsReferrerObject; + /** + * The action ID of the page view. + */ + actionId?: string; }; export type MetaMetricsPageOptions = { @@ -315,7 +381,7 @@ export type MetaMetricsUserTraits = { /** * Does the user have the Autodetect NFTs feature enabled? */ - nft_autodetection_enabled?: number; + nft_autodetection_enabled?: boolean; /** * A number representing the number of identities (accounts) added to the * user's wallet. @@ -354,10 +420,30 @@ export type MetaMetricsUserTraits = { * Does the user have token detection enabled? */ token_detection_enabled?: boolean; + /** + * Does the user have a selected currency in the settings + */ + current_currency?: string; + /** + * Does the user have show native token as main balance enabled. + */ + show_native_token_as_main_balance?: boolean; /** * Does the user have native currency enabled? */ use_native_as_primary_currency?: boolean; + /** + * Does the user opt in for metrics + */ + is_metrics_opted_in?: boolean; + /** + * Does the user accepted marketing consent + */ + has_marketing_consent?: boolean; + /** + * The date the extension was installed. + */ + install_date_ext?: string; /** * Whether the security provider feature has been enabled. */ @@ -366,7 +452,7 @@ export type MetaMetricsUserTraits = { /** * The address of the MMI account in question */ - mmi_account_address?: string; + mmi_account_address?: string | null; /** * What is the MMI extension ID */ @@ -376,6 +462,14 @@ export type MetaMetricsUserTraits = { */ mmi_is_custodian?: boolean; ///: END:ONLY_INCLUDE_IF + /** + * Does the user change the token sort order on the asset list + */ + token_sort_preference?: string; + /** + * The number of petname addresses + */ + petname_addresses_count?: number; }; export enum MetaMetricsUserTrait { @@ -524,6 +618,7 @@ export enum MetaMetricsEventName { BridgeLinkClicked = 'Bridge Link Clicked', BitcoinSupportToggled = 'Bitcoin Support Toggled', BitcoinTestnetSupportToggled = 'Bitcoin Testnet Support Toggled', + SolanaSupportToggled = 'Solana Support Toggled', CurrentCurrency = 'Current Currency', DappViewed = 'Dapp Viewed', DecryptionApproved = 'Decryption Approved', @@ -601,6 +696,7 @@ export enum MetaMetricsEventName { PetnameModalOpened = 'Petname Modal Opened', PetnameUpdated = 'Petname Updated', PhishingPageDisplayed = 'Phishing Page Displayed', + ProceedAnywayClicked = 'Proceed Anyway Clicked', PortfolioLinkClicked = 'Portfolio Link Clicked', ProviderMethodCalled = 'Provider Method Called', PublicAddressCopied = 'Public Address Copied', diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 67be9f72cee6..83998fe6e7b9 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -3,10 +3,20 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { CustodyController } from '@metamask-institutional/custody-controller'; 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 { + NetworkController, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; +import { + AccountsControllerGetAccountByAddressAction, + AccountsControllerSetAccountNameAction, + AccountsControllerListAccountsAction, + AccountsControllerGetSelectedAccountAction, + AccountsControllerSetSelectedAccountAction, +} from '@metamask/accounts-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { AppStateController } from '../../app/scripts/controllers/app-state-controller'; @@ -17,18 +27,48 @@ import AccountTrackerController from '../../app/scripts/controllers/account-trac // eslint-disable-next-line import/no-restricted-paths import MetaMetricsController from '../../app/scripts/controllers/metametrics'; +// Unique name for the controller +const controllerName = 'MMIController'; + +type NetworkControllerGetNetworkConfigurationByChainId = { + type: `NetworkController:getNetworkConfigurationByChainId`; + handler: NetworkController['getNetworkConfigurationByChainId']; +}; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | AccountsControllerGetAccountByAddressAction + | AccountsControllerSetAccountNameAction + | AccountsControllerListAccountsAction + | AccountsControllerGetSelectedAccountAction + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction + | NetworkControllerSetActiveNetworkAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetNetworkConfigurationByChainId; + +/** + * Messenger type for the {@link MMIController}. + */ +export type MMIControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AllowedActions, + never, + AllowedActions['type'], + never +>; + export type MMIControllerOptions = { mmiConfigurationController: MmiConfigurationController; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any keyringController: any; - preferencesController: PreferencesController; appStateController: AppStateController; transactionUpdateController: TransactionUpdateController; custodyController: CustodyController; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messenger: any; + messenger: MMIControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: () => any; diff --git a/shared/constants/multichain/assets.ts b/shared/constants/multichain/assets.ts index 988a9fbad624..23462d57d05a 100644 --- a/shared/constants/multichain/assets.ts +++ b/shared/constants/multichain/assets.ts @@ -2,9 +2,14 @@ import { MultichainNetworks } from './networks'; export const MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 = { BTC: `${MultichainNetworks.BITCOIN}/slip44:0`, + SOL: `${MultichainNetworks.SOLANA}/slip44:501`, } as const; export enum MultichainNativeAssets { BITCOIN = `${MultichainNetworks.BITCOIN}/slip44:0`, BITCOIN_TESTNET = `${MultichainNetworks.BITCOIN_TESTNET}/slip44:0`, + + SOLANA = `${MultichainNetworks.SOLANA}/slip44:501`, + SOLANA_DEVNET = `${MultichainNetworks.SOLANA_DEVNET}/slip44:501`, + SOLANA_TESTNET = `${MultichainNetworks.SOLANA_TESTNET}/slip44:501`, } diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index 5217394a5415..04ee1134c0b6 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -1,5 +1,9 @@ import { CaipChainId } from '@metamask/utils'; -import { isBtcMainnetAddress, isBtcTestnetAddress } from '../../lib/multichain'; +import { + isBtcMainnetAddress, + isBtcTestnetAddress, + isSolanaAddress, +} from '../../lib/multichain'; export type ProviderConfigWithImageUrl = { rpcUrl?: string; @@ -21,24 +25,39 @@ export type MultichainProviderConfig = ProviderConfigWithImageUrl & { export enum MultichainNetworks { BITCOIN = 'bip122:000000000019d6689c085ae165831e93', BITCOIN_TESTNET = 'bip122:000000000933ea01ad0ee984209779ba', + + SOLANA = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + SOLANA_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', + SOLANA_TESTNET = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z', } export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg'; +export const SOLANA_TOKEN_IMAGE_URL = './images/solana-logo.svg'; export const MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP = { [MultichainNetworks.BITCOIN]: 'https://blockstream.info/address', [MultichainNetworks.BITCOIN_TESTNET]: 'https://blockstream.info/testnet/address', + + [MultichainNetworks.SOLANA]: 'https://explorer.solana.com/', + [MultichainNetworks.SOLANA_DEVNET]: + 'https://explorer.solana.com/?cluster=devnet', + [MultichainNetworks.SOLANA_TESTNET]: + 'https://explorer.solana.com/?cluster=testnet', } as const; export const MULTICHAIN_TOKEN_IMAGE_MAP = { [MultichainNetworks.BITCOIN]: BITCOIN_TOKEN_IMAGE_URL, + [MultichainNetworks.SOLANA]: SOLANA_TOKEN_IMAGE_URL, } as const; export const MULTICHAIN_PROVIDER_CONFIGS: Record< CaipChainId, MultichainProviderConfig > = { + /** + * Bitcoin + */ [MultichainNetworks.BITCOIN]: { chainId: MultichainNetworks.BITCOIN, rpcUrl: '', // not used @@ -69,4 +88,53 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< }, isAddressCompatible: isBtcTestnetAddress, }, + /** + * Solana + */ + [MultichainNetworks.SOLANA]: { + chainId: MultichainNetworks.SOLANA, + rpcUrl: '', // not used + ticker: 'SOL', + nickname: 'Solana', + id: 'solana-mainnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[MultichainNetworks.SOLANA], + }, + isAddressCompatible: isSolanaAddress, + }, + [MultichainNetworks.SOLANA_DEVNET]: { + chainId: MultichainNetworks.SOLANA_DEVNET, + rpcUrl: '', // not used + ticker: 'SOL', + nickname: 'Solana (devnet)', + id: 'solana-devnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[ + MultichainNetworks.SOLANA_DEVNET + ], + }, + isAddressCompatible: isSolanaAddress, + }, + [MultichainNetworks.SOLANA_TESTNET]: { + chainId: MultichainNetworks.SOLANA_TESTNET, + rpcUrl: '', // not used + ticker: 'SOL', + nickname: 'Solana (testnet)', + id: 'solana-testnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[ + MultichainNetworks.SOLANA_TESTNET + ], + }, + isAddressCompatible: isSolanaAddress, + }, }; diff --git a/shared/constants/network.test.ts b/shared/constants/network.test.ts index 20a13ccfb273..9d9ca72b46cd 100644 --- a/shared/constants/network.test.ts +++ b/shared/constants/network.test.ts @@ -35,6 +35,7 @@ describe('NetworkConstants', () => { 'Polygon Mainnet': CHAIN_IDS.POLYGON, 'zkSync Era Mainnet': CHAIN_IDS.ZKSYNC_ERA, 'Base Mainnet': CHAIN_IDS.BASE, + 'Linea Mainnet': CHAIN_IDS.LINEA_MAINNET, }; FEATURED_RPCS.forEach((rpc) => { diff --git a/shared/constants/network.ts b/shared/constants/network.ts index e911ce1aabf5..4844e7c2e981 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -147,11 +147,14 @@ export const CHAIN_IDS = { NUMBERS: '0x290b', SEI: '0x531', APE_TESTNET: '0x8157', + APE_MAINNET: '0x8173', BERACHAIN: '0x138d5', METACHAIN_ONE: '0x1b6e6', ARBITRUM_SEPOLIA: '0x66eee', NEAR: '0x18d', NEAR_TESTNET: '0x18e', + GRAVITY_ALPHA_MAINNET: '0x659', + GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', } as const; export const CHAINLIST_CHAIN_IDS_MAP = { @@ -207,6 +210,9 @@ export const CHAINLIST_CHAIN_IDS_MAP = { ZORA_MAINNET: '0x76adf1', FILECOIN: '0x13a', NUMBERS: '0x290b', + APE: '0x8173', + GRAVITY_ALPHA_MAINNET: '0x659', + GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', } as const; // To add a deprecation warning to a network, add it to the array @@ -371,6 +377,7 @@ const CHAINLIST_CURRENCY_SYMBOLS_MAP = { HUOBI_ECO_CHAIN_MAINNET: 'HT', ACALA_NETWORK: 'ACA', IOTEX_MAINNET: 'IOTX', + APE: 'APE', } as const; export const CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION = { @@ -416,6 +423,7 @@ export const FUSE_GOLD_MAINNET_IMAGE_URL = './images/fuse-mainnet.jpg'; export const HAQQ_NETWORK_IMAGE_URL = './images/haqq.svg'; export const IOTEX_MAINNET_IMAGE_URL = './images/iotex.svg'; export const IOTEX_TOKEN_IMAGE_URL = './images/iotex-token.svg'; +export const APE_TOKEN_IMAGE_URL = './images/ape-token.svg'; export const KCC_MAINNET_IMAGE_URL = './images/kcc-mainnet.svg'; export const KLAYTN_MAINNET_IMAGE_URL = './images/klaytn.svg'; export const KROMA_MAINNET_IMAGE_URL = './images/kroma.svg'; @@ -449,7 +457,9 @@ 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 APE_IMAGE_URL = './images/ape.svg'; +export const GRAVITY_ALPHA_MAINNET_IMAGE_URL = './images/gravity.svg'; +export const GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL = './images/gravity.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -564,6 +574,7 @@ export const NETWORK_TO_NAME_MAP = { export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.AVALANCHE]: CHAINLIST_CURRENCY_SYMBOLS_MAP.AVALANCHE, + [CHAINLIST_CHAIN_IDS_MAP.APE]: CHAINLIST_CURRENCY_SYMBOLS_MAP.APE, [CHAINLIST_CHAIN_IDS_MAP.BSC]: CHAINLIST_CURRENCY_SYMBOLS_MAP.BNB, [CHAINLIST_CHAIN_IDS_MAP.BASE]: CHAINLIST_CURRENCY_SYMBOLS_MAP.BASE, [CHAINLIST_CHAIN_IDS_MAP.ARBITRUM]: CHAINLIST_CURRENCY_SYMBOLS_MAP.ARBITRUM, @@ -782,10 +793,15 @@ 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.APE_TESTNET]: APE_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.APE_MAINNET]: APE_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, + [CHAINLIST_CHAIN_IDS_MAP.GRAVITY_ALPHA_MAINNET]: + GRAVITY_ALPHA_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.GRAVITY_ALPHA_TESTNET_SEPOLIA]: + GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL, } as const; export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP = { @@ -817,6 +833,10 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAIN_IDS.MOONRIVER]: MOONRIVER_TOKEN_IMAGE_URL, [CHAIN_IDS.MOONBEAM]: MOONBEAM_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.IOTEX_MAINNET]: IOTEX_TOKEN_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.APE_MAINNET]: APE_TOKEN_IMAGE_URL, + [CHAIN_IDS.GRAVITY_ALPHA_MAINNET]: GRAVITY_ALPHA_MAINNET_IMAGE_URL, + [CHAIN_IDS.GRAVITY_ALPHA_TESTNET_SEPOLIA]: + GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL, } as const; export const INFURA_BLOCKED_KEY = 'countryBlocked'; @@ -932,6 +952,20 @@ export const UNSUPPORTED_RPC_METHODS = new Set([ export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link'; export const FEATURED_RPCS: AddNetworkFields[] = [ + { + chainId: CHAIN_IDS.LINEA_MAINNET, + name: LINEA_MAINNET_DISPLAY_NAME, + nativeCurrency: CURRENCY_SYMBOLS.ETH, + rpcEndpoints: [ + { + url: `https://linea-mainnet.infura.io/v3/${infuraProjectId}`, + type: RpcEndpointType.Custom, + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://lineascan.build/'], + defaultBlockExplorerUrlIndex: 0, + }, { chainId: CHAIN_IDS.ARBITRUM, name: ARBITRUM_DISPLAY_NAME, diff --git a/shared/constants/security-provider.ts b/shared/constants/security-provider.ts index e6fff53ee28a..082f68aa88de 100644 --- a/shared/constants/security-provider.ts +++ b/shared/constants/security-provider.ts @@ -89,7 +89,7 @@ export const FALSE_POSITIVE_REPORT_BASE_URL = export const SECURITY_PROVIDER_UTM_SOURCE = 'metamask-ppom'; -export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS: Hex[] = [ +export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST: Hex[] = [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.AVALANCHE, CHAIN_IDS.BASE, diff --git a/shared/constants/swaps.ts b/shared/constants/swaps.ts index 3868c7b6e2f0..8dfecccef6e6 100644 --- a/shared/constants/swaps.ts +++ b/shared/constants/swaps.ts @@ -49,10 +49,6 @@ export type SwapsTokenObject = { iconUrl: string; }; -type BlockExplorerUrlMap = { - [key: string]: string; -}; - export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { symbol: CURRENCY_SYMBOLS.ETH, name: 'Ether', @@ -174,17 +170,6 @@ export const TOKEN_API_BASE_URL = 'https://tokens.api.cx.metamask.io'; export const GAS_API_BASE_URL = 'https://gas.api.cx.metamask.io'; export const GAS_DEV_API_BASE_URL = 'https://gas.uat-api.cx.metamask.io'; -const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; -export const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; -const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/'; -const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; -const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; -const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/'; -const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/'; -const ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL = 'https://explorer.zksync.io/'; -export const LINEA_DEFAULT_BLOCK_EXPLORER_URL = 'https://lineascan.build/'; -const BASE_DEFAULT_BLOCK_EXPLORER_URL = 'https://basescan.org/'; - export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [ CHAIN_IDS.MAINNET, SWAPS_TESTNET_CHAIN_ID, @@ -298,20 +283,6 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, } as const; -export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP: BlockExplorerUrlMap = - { - [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_URL, - } as const; - export const ETHEREUM = 'ethereum'; export const POLYGON = 'polygon'; export const BSC = 'bsc'; diff --git a/shared/lib/accounts/solana-wallet-snap.ts b/shared/lib/accounts/solana-wallet-snap.ts new file mode 100644 index 000000000000..1f6622c8bbdc --- /dev/null +++ b/shared/lib/accounts/solana-wallet-snap.ts @@ -0,0 +1,9 @@ +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 SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; + +export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId; + +export const SOLANA_WALLET_NAME: string = + SolanaWalletSnap.manifest.proposedName; diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js index 89caef28026d..66efd6fb61fb 100644 --- a/shared/lib/error-utils.js +++ b/shared/lib/error-utils.js @@ -5,17 +5,27 @@ import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n'; import switchDirection from './switch-direction'; +const defaultLocale = 'en'; const _setupLocale = async (currentLocale) => { - const currentLocaleMessages = currentLocale - ? await fetchLocale(currentLocale) - : {}; - const enLocaleMessages = await fetchLocale('en'); + const enRelativeTime = loadRelativeTimeFormatLocaleData(defaultLocale); + const enLocale = fetchLocale(defaultLocale); - await loadRelativeTimeFormatLocaleData('en'); - if (currentLocale) { - await loadRelativeTimeFormatLocaleData(currentLocale); + const promises = [enRelativeTime, enLocale]; + if (currentLocale === defaultLocale) { + // enLocaleMessages and currentLocaleMessages are the same; reuse enLocale + promises.push(enLocale); // currentLocaleMessages + } else if (currentLocale) { + // currentLocale does not match enLocaleMessages + promises.push(fetchLocale(currentLocale)); // currentLocaleMessages + promises.push(loadRelativeTimeFormatLocaleData(currentLocale)); + } else { + // currentLocale is not set + promises.push(Promise.resolve({})); // currentLocaleMessages } + const [, enLocaleMessages, currentLocaleMessages] = await Promise.all( + promises, + ); return { currentLocaleMessages, enLocaleMessages }; }; diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts index 4c1bab12d03b..6e8abd726fb8 100644 --- a/shared/lib/multichain.test.ts +++ b/shared/lib/multichain.test.ts @@ -3,6 +3,7 @@ import { getCaipNamespaceFromAddress, isBtcMainnetAddress, isBtcTestnetAddress, + isSolanaAddress, } from './multichain'; const BTC_MAINNET_ADDRESSES = [ @@ -61,10 +62,24 @@ describe('multichain', () => { ); }); + describe('isSolanaAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(SOL_ADDRESSES)( + 'returns true if address is a valid Solana address: %s', + (address: string) => { + expect(isSolanaAddress(address)).toBe(true); + }, + ); + + it('should return false for invalid Solana addresses', () => { + expect(isSolanaAddress('invalid')).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', + 'returns KnownCaipNamespace.Bitcoin for bitcoin address: %s', (address: string) => { expect(getCaipNamespaceFromAddress(address)).toBe( KnownCaipNamespace.Bip122, @@ -74,7 +89,7 @@ describe('multichain', () => { // @ts-expect-error This is missing from the Mocha type definitions it.each(ETH_ADDRESSES)( - 'returns ChainType.Ethereum for ethereum address: %s', + 'returns KnownCaipNamespace.Ethereum for ethereum address: %s', (address: string) => { expect(getCaipNamespaceFromAddress(address)).toBe( KnownCaipNamespace.Eip155, @@ -84,10 +99,10 @@ describe('multichain', () => { // @ts-expect-error This is missing from the Mocha type definitions it.each(SOL_ADDRESSES)( - 'returns ChainType.Ethereum for non-supported address: %s', + 'returns KnownCaipNamespace.Solana for non-supported address: %s', (address: string) => { expect(getCaipNamespaceFromAddress(address)).toBe( - KnownCaipNamespace.Eip155, + KnownCaipNamespace.Solana, ); }, ); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts index 942a9ce6c964..815d9d9e6763 100644 --- a/shared/lib/multichain.ts +++ b/shared/lib/multichain.ts @@ -1,5 +1,6 @@ import { CaipNamespace, KnownCaipNamespace } from '@metamask/utils'; import { validate, Network } from 'bitcoin-address-validation'; +import { isAddress } from '@solana/addresses'; /** * Returns whether an address is on the Bitcoin mainnet. @@ -28,6 +29,18 @@ export function isBtcTestnetAddress(address: string): boolean { return validate(address, Network.testnet); } +/** + * Returns whether an address is a valid Solana address, specifically an account's. + * Derived addresses (like Program's) will return false. + * See: https://stackoverflow.com/questions/71200948/how-can-i-validate-a-solana-wallet-address-with-web3js + * + * @param address - The address to check. + * @returns `true` if the address is a valid Solana address, `false` otherwise. + */ +export function isSolanaAddress(address: string): boolean { + return isAddress(address); +} + /** * Returns the associated chain's type for the given address. * @@ -38,6 +51,11 @@ export function getCaipNamespaceFromAddress(address: string): CaipNamespace { if (isBtcMainnetAddress(address) || isBtcTestnetAddress(address)) { return KnownCaipNamespace.Bip122; } + + if (isSolanaAddress(address)) { + return KnownCaipNamespace.Solana; + } + // Defaults to "Ethereum" for all other cases for now. return KnownCaipNamespace.Eip155; } diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts new file mode 100644 index 000000000000..dc80bfd9a92a --- /dev/null +++ b/shared/modules/add-nonce-to-csp.test.ts @@ -0,0 +1,98 @@ +import { addNonceToCsp } from './add-nonce-to-csp'; + +describe('addNonceToCsp', () => { + it('empty string', () => { + const input = ''; + const expected = ''; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one empty directive', () => { + const input = 'script-src'; + const expected = `script-src 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, one value', () => { + const input = 'script-src default.example'; + const expected = `script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, two values', () => { + const input = "script-src 'self' default.example"; + const expected = `script-src 'self' default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('multiple directives', () => { + const input = + "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example"; + const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('no applicable directive', () => { + const input = 'img-src https://example.com'; + const expected = `img-src https://example.com`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('non-ASCII directives', () => { + const input = 'script-src default.example;\u0080;style-src style.example'; + const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('uppercase directive names', () => { + const input = 'SCRIPT-SRC DEFAULT.EXAMPLE'; + const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('duplicate directive names', () => { + const input = + 'default-src default.example; script-src script.example; script-src script.example'; + const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('nonce value contains script-src', () => { + const input = + "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com"; + const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('url value contains script-src', () => { + const input = + "default-src 'self' https://script-src.com; script-src 'self' https://example.com"; + const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('fallback to default-src', () => { + const input = `default-src 'none'`; + const expected = `default-src 'none' 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('keep ascii whitespace characters', () => { + const input = ' script-src default.example '; + const expected = ` script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); +}); diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts new file mode 100644 index 000000000000..a8b7fe333089 --- /dev/null +++ b/shared/modules/add-nonce-to-csp.ts @@ -0,0 +1,38 @@ +// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. +// See . +const ASCII_WHITESPACE_CHARS = ['\t', '\n', '\f', '\r', ' '].join(''); + +const matchDirective = (directive: string) => + /* eslint-disable require-unicode-regexp */ + new RegExp( + `^([${ASCII_WHITESPACE_CHARS}]*${directive}[${ASCII_WHITESPACE_CHARS}]*)`, // Match the directive and surrounding ASCII whitespace + 'is', // Case-insensitive, including newlines + ); +const matchScript = matchDirective('script-src'); +const matchDefault = matchDirective('default-src'); + +/** + * Adds a nonce to a Content Security Policy (CSP) string. + * + * @param text - The Content Security Policy (CSP) string to add the nonce to. + * @param nonce - The nonce to add to the Content Security Policy (CSP) string. + * @returns The updated Content Security Policy (CSP) string. + */ +export const addNonceToCsp = (text: string, nonce: string) => { + const formattedNonce = ` 'nonce-${nonce}'`; + const directives = text.split(';'); + const scriptIndex = directives.findIndex((directive) => + matchScript.test(directive), + ); + if (scriptIndex >= 0) { + directives[scriptIndex] += formattedNonce; + } else { + const defaultIndex = directives.findIndex((directive) => + matchDefault.test(directive), + ); + if (defaultIndex >= 0) { + directives[defaultIndex] += formattedNonce; + } + } + return directives.join(';'); +}; diff --git a/shared/modules/i18n.test.ts b/shared/modules/i18n.test.ts index 8452ef48238c..7a2f49bd9f68 100644 --- a/shared/modules/i18n.test.ts +++ b/shared/modules/i18n.test.ts @@ -109,7 +109,21 @@ describe('I18N Module', () => { ); }); - it('throws if test env set', () => { + it('throws if IN_TEST is set true', () => { + expect(() => + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toThrow( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ); + }); + + it('throws if ENABLE_SETTINGS_PAGE_DEV_OPTIONS is set true', () => { + process.env.IN_TEST = String(false); + process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS = String(true); expect(() => getMessage( FALLBACK_LOCALE, diff --git a/shared/modules/i18n.ts b/shared/modules/i18n.ts index d19cfa4b3b11..b5c22c869b54 100644 --- a/shared/modules/i18n.ts +++ b/shared/modules/i18n.ts @@ -177,7 +177,7 @@ function missingKeyError( onError?.(error); log.error(error); - if (process.env.IN_TEST) { + if (process.env.IN_TEST || process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) { throw error; } } @@ -188,7 +188,6 @@ function missingKeyError( warned[localeCode] = warned[localeCode] ?? {}; warned[localeCode][key] = true; - log.warn( `Translator - Unable to find value of key "${key}" for locale "${localeCode}"`, ); diff --git a/shared/modules/provider-injection.js b/shared/modules/provider-injection.js index 25a316e93440..b96df88c29e2 100644 --- a/shared/modules/provider-injection.js +++ b/shared/modules/provider-injection.js @@ -5,40 +5,36 @@ */ export default function shouldInjectProvider() { return ( - doctypeCheck() && - suffixCheck() && - documentElementCheck() && - !blockedDomainCheck() + checkURLForProviderInjection(new URL(window.location)) && + checkDocumentForProviderInjection() ); } /** - * Checks the doctype of the current document if it exists + * Checks if a given URL is eligible for provider injection. * - * @returns {boolean} {@code true} if the doctype is html or if none exists + * This function determines if a URL passes the suffix check and is not part of the blocked domains. + * + * @param {URL} url - The URL to be checked for injection. + * @returns {boolean} Returns `true` if the URL passes the suffix check and is not blocked, otherwise `false`. */ -function doctypeCheck() { - const { doctype } = window.document; - if (doctype) { - return doctype.name === 'html'; - } - return true; +export function checkURLForProviderInjection(url) { + return suffixCheck(url) && !blockedDomainCheck(url); } /** - * Returns whether or not the extension (suffix) of the current document is prohibited + * Returns whether or not the extension (suffix) of the given URL's pathname is prohibited * - * This checks {@code window.location.pathname} against a set of file extensions - * that we should not inject the provider into. This check is indifferent of - * query parameters in the location. + * This checks the provided URL's pathname against a set of file extensions + * that we should not inject the provider into. * - * @returns {boolean} whether or not the extension of the current document is prohibited + * @param {URL} url - The URL to check + * @returns {boolean} whether or not the extension of the given URL's pathname is prohibited */ -function suffixCheck() { +function suffixCheck({ pathname }) { const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]; - const currentUrl = window.location.pathname; for (let i = 0; i < prohibitedTypes.length; i++) { - if (prohibitedTypes[i].test(currentUrl)) { + if (prohibitedTypes[i].test(pathname)) { return false; } } @@ -46,24 +42,12 @@ function suffixCheck() { } /** - * Checks the documentElement of the current document + * Checks if the given domain is blocked * - * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + * @param {URL} url - The URL to check + * @returns {boolean} {@code true} if the given domain is blocked */ -function documentElementCheck() { - const documentElement = document.documentElement.nodeName; - if (documentElement) { - return documentElement.toLowerCase() === 'html'; - } - return true; -} - -/** - * Checks if the current domain is blocked - * - * @returns {boolean} {@code true} if the current domain is blocked - */ -function blockedDomainCheck() { +function blockedDomainCheck(url) { // If making any changes, please also update the same list found in the MetaMask-Mobile & SDK repositories const blockedDomains = [ 'execution.consensys.io', @@ -85,8 +69,7 @@ function blockedDomainCheck() { 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', ]; - const { hostname: currentHostname, pathname: currentPathname } = - window.location; + const { hostname: currentHostname, pathname: currentPathname } = url; const trimTrailingSlash = (str) => str.endsWith('/') ? str.slice(0, -1) : str; @@ -104,3 +87,38 @@ function blockedDomainCheck() { ) ); } + +/** + * Checks if the document is suitable for provider injection by verifying the doctype and document element. + * + * @returns {boolean} `true` if the document passes both the doctype and document element checks, otherwise `false`. + */ +export function checkDocumentForProviderInjection() { + return doctypeCheck() && documentElementCheck(); +} + +/** + * Checks the doctype of the current document if it exists + * + * @returns {boolean} {@code true} if the doctype is html or if none exists + */ +function doctypeCheck() { + const { doctype } = window.document; + if (doctype) { + return doctype.name === 'html'; + } + return true; +} + +/** + * Checks the documentElement of the current document + * + * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + */ +function documentElementCheck() { + const documentElement = document.documentElement.nodeName; + if (documentElement) { + return documentElement.toLowerCase() === 'html'; + } + return true; +} diff --git a/shared/modules/provider-injection.test.ts b/shared/modules/provider-injection.test.ts index 1742e31b4db6..c7da24b46642 100644 --- a/shared/modules/provider-injection.test.ts +++ b/shared/modules/provider-injection.test.ts @@ -8,11 +8,7 @@ describe('shouldInjectProvider', () => { const urlObj = new URL(urlString); mockedWindow.mockImplementation(() => ({ - location: { - hostname: urlObj.hostname, - origin: urlObj.origin, - pathname: urlObj.pathname, - }, + location: urlObj, document: { doctype: { name: 'html', diff --git a/shared/modules/selectors/index.test.ts b/shared/modules/selectors/index.test.ts index 9f0b1b201a5c..2e40d47db102 100644 --- a/shared/modules/selectors/index.test.ts +++ b/shared/modules/selectors/index.test.ts @@ -9,7 +9,6 @@ import { getCurrentChainSupportsSmartTransactions, getSmartTransactionsEnabled, getIsSmartTransaction, - getIsSmartTransactionsOptInModalAvailable, getSmartTransactionsPreferenceEnabled, } from '.'; @@ -70,7 +69,7 @@ describe('Selectors', () => { }; describe('getSmartTransactionsOptInStatusForMetrics and getSmartTransactionsPreferenceEnabled', () => { - const createMockOptInStatusState = (status: boolean | null) => { + const createMockOptInStatusState = (status: boolean) => { return { metamask: { preferences: { @@ -89,7 +88,6 @@ describe('Selectors', () => { 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 }) => { @@ -113,7 +111,6 @@ describe('Selectors', () => { 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 }) => { @@ -316,123 +313,4 @@ describe('Selectors', () => { expect(result).toBe(false); }); }); - - describe('getIsSmartTransactionsOptInModalAvailable', () => { - 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); - }, - ); - - 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 }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }, - ); - - 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/', - }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).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); - }, - ); - - 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); - }, - ); - - 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); - }, - ); - }); }); diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index a02fe63692b3..4fb6d56fc87d 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -7,21 +7,16 @@ import { getCurrentChainId, 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. import { isProduction } from '../environment'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { MultichainState } from '../../../ui/selectors/multichain'; - type SmartTransactionsMetaMaskState = { metamask: { preferences: { - smartTransactionsOptInStatus?: boolean | null; + smartTransactionsOptInStatus?: boolean; }; internalAccounts: { selectedAccount: string; @@ -72,10 +67,8 @@ type SmartTransactionsMetaMaskState = { */ export const getSmartTransactionsOptInStatusInternal = createSelector( getPreferences, - (preferences: { - smartTransactionsOptInStatus?: boolean | null; - }): boolean | null => { - return preferences?.smartTransactionsOptInStatus ?? null; + (preferences: { smartTransactionsOptInStatus?: boolean }): boolean => { + return preferences?.smartTransactionsOptInStatus ?? true; }, ); @@ -93,7 +86,7 @@ export const getSmartTransactionsOptInStatusInternal = createSelector( */ export const getSmartTransactionsOptInStatusForMetrics = createSelector( getSmartTransactionsOptInStatusInternal, - (optInStatus: boolean | null): boolean | null => optInStatus, + (optInStatus: boolean): boolean => optInStatus, ); /** @@ -105,7 +98,7 @@ export const getSmartTransactionsOptInStatusForMetrics = createSelector( */ export const getSmartTransactionsPreferenceEnabled = createSelector( getSmartTransactionsOptInStatusInternal, - (optInStatus: boolean | null): boolean => { + (optInStatus: boolean): boolean => { // In the absence of an explicit opt-in or opt-out, // the Smart Transactions toggle is enabled. const DEFAULT_SMART_TRANSACTIONS_ENABLED = true; @@ -137,30 +130,6 @@ const getIsAllowedRpcUrlForSmartTransactions = ( return rpcUrl?.hostname?.endsWith('.infura.io'); }; -/** - * Checks if the selected account has a non-zero balance. - * - * @param state - The state object containing account information. - * @returns true if the selected account has a non-zero balance, otherwise false. - */ -const hasNonZeroBalance = (state: SmartTransactionsMetaMaskState) => { - const selectedAccount = getSelectedAccount( - state as unknown as MultichainState, - ); - return BigInt(selectedAccount?.balance || '0x0') > 0n; -}; - -export const getIsSmartTransactionsOptInModalAvailable = ( - state: SmartTransactionsMetaMaskState, -) => { - return ( - getCurrentChainSupportsSmartTransactions(state) && - getIsAllowedRpcUrlForSmartTransactions(state) && - getSmartTransactionsOptInStatusInternal(state) === null && - hasNonZeroBalance(state) - ); -}; - export const getSmartTransactionsEnabled = ( state: SmartTransactionsMetaMaskState, ): boolean => { diff --git a/test/data/bridge/mock-quotes-erc20-erc20.json b/test/data/bridge/mock-quotes-erc20-erc20.json new file mode 100644 index 000000000000..8b589aa85e1b --- /dev/null +++ b/test/data/bridge/mock-quotes-erc20-erc20.json @@ -0,0 +1,248 @@ +[ + { + "quote": { + "requestId": "90ae8e69-f03a-4cf6-bab7-ed4e3431eb37", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + }, + "srcTokenAmount": "14000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "13984280", + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["across"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "14000000", + "destAmount": "13984280" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "approval": { + "chainId": 10, + "to": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000d59f80", + "gasLimit": 61865 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x038d7ea4c68000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000890000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000004a0c3540448000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000019d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284792ebcb90000000000000000000000000000000000000000000000000000000000d59f80000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000454000000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000d55a40000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000067041c47000000000000000000000000000000000000000000000000000000006704704d00000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef765753be7f7a64d5509974b0d678e1e3149b02f42c7402906f9888136205038026f20b3f6df2899044cab41d632bc7a6c35debd40516df85de6f194aeb05b72cb9ea4d5ce0f7c56c91a79536331112f1a846dc641c", + "gasLimit": 287227 + }, + "estimatedProcessingTimeInSeconds": 60 + }, + { + "quote": { + "requestId": "0b6caac9-456d-47e6-8982-1945ae81ae82", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + }, + "srcTokenAmount": "14000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "13800000", + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["celercircle"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "cctp", + "displayName": "Circle CCTP", + "icon": "https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "14000000", + "destAmount": "13800000" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "approval": { + "chainId": 10, + "to": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000d59f80", + "gasLimit": 61865 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x038d7ea4c68000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000890000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000002e4c3540448000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e4b7dfe9d00000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000138bc5930d51a475e4669db259f69e61ca33803675e76540f062a76af8cbaef4672c9926e56d6a8c29a263de3ee8f734ad760461c448f82fdccdd8c2360fffba1b", + "gasLimit": 343079 + }, + "estimatedProcessingTimeInSeconds": 1560 + } +] diff --git a/test/data/bridge/mock-quotes-native-erc20.json b/test/data/bridge/mock-quotes-native-erc20.json new file mode 100644 index 000000000000..fb6ecfcc0b73 --- /dev/null +++ b/test/data/bridge/mock-quotes-native-erc20.json @@ -0,0 +1,294 @@ +[ + { + "quote": { + "requestId": "381c23bc-e3e4-48fe-bc53-257471e388ad", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcTokenAmount": "9912500000000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "24438902", + "feeData": { + "metabridge": { + "amount": "87500000000000", + "asset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["across"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "protocol": { + "name": "zerox", + "displayName": "0x", + "icon": "https://media.socket.tech/dexes/0x.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://assets.polygon.technology/tokenAssets/eth.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/eth.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "srcAmount": "9912500000000000", + "destAmount": "24456223" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "24456223", + "destAmount": "24438902" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x27147114878000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f600000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000e2037c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000d64123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000019d0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000001000000000000000000000000000000000000011000000000000000000000000000000000000000021582def464917822ff6092c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000043a900000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000174e7be000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000067041c47000000000000000000000000000000000000000000000000000000006704704d00000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef765753be7f7a64d5509974b0d678e1e3149b02f41fec59a4aef7d9ac92ee5eeaf293cb28c2261e7fd322723a97cb83762f7302296636026e52849fdad0f9db6e1640f914660e6b13f5b1a29345344c8c5687abbf1b", + "gasLimit": 610414 + }, + "estimatedProcessingTimeInSeconds": 60 + }, + { + "quote": { + "requestId": "4277a368-40d7-4e82-aa67-74f29dc5f98a", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcTokenAmount": "9912500000000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "24256223", + "feeData": { + "metabridge": { + "amount": "87500000000000", + "asset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["celercircle"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "protocol": { + "name": "zerox", + "displayName": "0x", + "icon": "https://media.socket.tech/dexes/0x.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://assets.polygon.technology/tokenAssets/eth.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/eth.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "srcAmount": "9912500000000000", + "destAmount": "24456223" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "cctp", + "displayName": "Circle CCTP", + "icon": "https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "24456223", + "destAmount": "24256223" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x27147114878000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000c6437c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc4123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd00000000000000000000000010000000000000000000000000000000000000110000000000000000000000000000000000000000974132b87a5cb75e32f034280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000030d4000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f9e43204a24f476db20f2518722627a122d31a1bc7c63fc15412e6a327295a9460b76bea5bb53b1f73fa6a15811055f6bada592d2e9e6c8cf48a855ce6968951c", + "gasLimit": 664389 + }, + "estimatedProcessingTimeInSeconds": 1560 + } +] diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 49a6e1aad1ab..cbe5dd2bd2ba 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -1,4 +1,5 @@ import { + CHAIN_IDS, SimulationData, TransactionMeta, TransactionStatus, @@ -18,7 +19,7 @@ export const CONTRACT_INTERACTION_SENDER_ADDRESS = export const DEPOSIT_METHOD_DATA = '0xd0e30db0'; -export const CHAIN_ID = '0xaa36a7'; +export const CHAIN_ID = CHAIN_IDS.GOERLI; export const genUnapprovedContractInteractionConfirmation = ({ address = CONTRACT_INTERACTION_SENDER_ADDRESS, diff --git a/test/data/confirmations/helper.ts b/test/data/confirmations/helper.ts index 6669c043d0ea..b8bd8a634588 100644 --- a/test/data/confirmations/helper.ts +++ b/test/data/confirmations/helper.ts @@ -133,6 +133,7 @@ export const getMockConfirmState = (args: RootState = { metamask: {} }) => ({ ...args.metamask, preferences: { ...mockState.metamask.preferences, + ...(args.metamask?.preferences as Record), redesignedTransactionsEnabled: true, redesignedConfirmationsEnabled: true, isRedesignedConfirmationsDeveloperEnabled: true, diff --git a/test/data/confirmations/personal_sign.ts b/test/data/confirmations/personal_sign.ts index b0f135efa53d..69c3ff9f75bc 100644 --- a/test/data/confirmations/personal_sign.ts +++ b/test/data/confirmations/personal_sign.ts @@ -1,3 +1,4 @@ +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm'; export const PERSONAL_SIGN_SENDER_ADDRESS = @@ -5,6 +6,7 @@ export const PERSONAL_SIGN_SENDER_ADDRESS = export const unapprovedPersonalSignMsg = { id: '0050d5b0-c023-11ee-a0cb-3390a510a0ab', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: new Date().getTime(), type: 'personal_sign', @@ -20,6 +22,7 @@ export const unapprovedPersonalSignMsg = { export const signatureRequestSIWE = { id: '210ca3b0-1ccb-11ef-b096-89c4d726ebb5', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -57,6 +60,7 @@ export const signatureRequestSIWE = { export const SignatureRequestSIWEWithResources = { id: '210ca3b0-1ccb-11ef-b096-89c4d726ebb5', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index 7be24a1389c6..831d561f0cb2 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -1,9 +1,10 @@ -import { TransactionType } from '@metamask/transaction-controller'; +import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm'; export const unapprovedTypedSignMsgV1 = { id: '82ab2400-e2c6-11ee-9627-73cc88f00492', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -60,6 +61,7 @@ const rawMessageV3 = { export const unapprovedTypedSignMsgV3 = { id: '17e41af0-e073-11ee-9eec-5fd284826685', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -129,6 +131,7 @@ export const rawMessageV4 = { export const unapprovedTypedSignMsgV4 = { id: '0050d5b0-c023-11ee-a0cb-3390a510a0ab', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: new Date().getTime(), chainid: '0x5', @@ -145,6 +148,7 @@ export const unapprovedTypedSignMsgV4 = { export const orderSignatureMsg = { id: 'e5249ae0-4b6b-11ef-831f-65b48eb489ec', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { result_type: 'loading', reason: 'validation_in_progress', @@ -165,6 +169,7 @@ export const orderSignatureMsg = { export const permitSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -185,6 +190,7 @@ export const permitSignatureMsg = { export const permitNFTSignatureMsg = { id: 'c5067710-87cf-11ef-916c-71f266571322', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: 1728651190529, type: 'eth_signTypedData', @@ -200,6 +206,7 @@ export const permitNFTSignatureMsg = { export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -219,6 +226,7 @@ export const permitSignatureMsgWithNoDeadline = { export const permitBatchSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -239,6 +247,7 @@ export const permitBatchSignatureMsg = { export const permitSingleSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 654e915a1305..80e5499447d7 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -46,7 +46,13 @@ "mostRecentOverviewPage": "/mostRecentOverviewPage" }, "localeMessages": { - "currentLocale": "en" + "currentLocale": "en", + "current": { + "user": "user" + }, + "en": { + "user": "user" + } }, "metamask": { "use4ByteResolution": true, @@ -372,7 +378,7 @@ "showFiatInTestnets": false, "showNativeTokenAsMainBalance": true, "showTestNetworks": true, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "tokenSortConfig": { "key": "tokenFiatAmount", "order": "dsc", @@ -420,6 +426,7 @@ "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", "id": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3", "metadata": { + "importTime": 0, "name": "Test Account", "keyring": { "type": "HD Key Tree" @@ -439,6 +446,7 @@ "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", "id": "07c2cfec-36c9-46c4-8115-3836d3ac9047", "metadata": { + "importTime": 0, "name": "Test Account 2", "keyring": { "type": "HD Key Tree" @@ -458,6 +466,7 @@ "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", "id": "15e69915-2a1a-4019-93b3-916e11fd432f", "metadata": { + "importTime": 0, "name": "Ledger Hardware 2", "keyring": { "type": "Ledger Hardware" @@ -477,6 +486,7 @@ "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823", "id": "784225f4-d30b-4e77-a900-c8bbce735b88", "metadata": { + "importTime": 0, "name": "Test Account 3", "keyring": { "type": "HD Key Tree" @@ -496,6 +506,7 @@ "address": "0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281", "id": "694225f4-d30b-4e77-a900-c8bbce735b42", "metadata": { + "importTime": 0, "name": "Test Account 4", "keyring": { "type": "Custody test" @@ -515,11 +526,13 @@ "address": "0xb552685e3d2790efd64a175b00d51f02cdafee5d", "id": "c3deeb99-ba0d-4a4e-a0aa-033fc1f79ae3", "metadata": { + "importTime": 0, "name": "Snap Account 1", "keyring": { "type": "Snap Keyring" }, "snap": { + "enabled": true, "id": "snap-id", "name": "snap-name" } diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index c3957cb6fbbf..8bf39d261bcb 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -41,6 +41,7 @@ export const DEFAULT_GANACHE_ETH_BALANCE_DEC = '25'; /* Dapp host addresses and URL*/ export const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; +export const DAPP_URL_LOCALHOST = 'http://localhost:8080'; export const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; export const DAPP_ONE_URL = 'http://127.0.0.1:8081'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 95f35bf1694c..fd2d5be42891 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -193,6 +193,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { currentLocale: 'en', useExternalServices: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: { @@ -212,7 +213,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index d73b959946c2..bea9e9bad77f 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -13,6 +13,7 @@ const { CHAIN_IDS } = require('../../shared/constants/network'); const { SMART_CONTRACTS } = require('./seeder/smart-contracts'); const { DAPP_URL, + DAPP_URL_LOCALHOST, DAPP_ONE_URL, DEFAULT_FIXTURE_ACCOUNT, ERC_4337_ACCOUNT, @@ -57,10 +58,12 @@ function onboardingFixture() { }), providerConfig: { id: 'networkConfigurationId' }, }, + NotificationServicesController: {}, PreferencesController: { advancedGasFee: {}, currentLocale: 'en', dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: {}, @@ -73,8 +76,9 @@ function onboardingFixture() { hideZeroBalanceTokens: false, showExtensionInFullSizeView: false, showFiatInTestnets: false, + privacyMode: false, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, @@ -121,7 +125,7 @@ function onboardingFixture() { [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, }, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, }, QueuedRequestController: { queuedRequestCount: 0, @@ -138,6 +142,7 @@ function onboardingFixture() { }, }, }, + UserStorageController: {}, TokensController: { allDetectedTokens: {}, allIgnoredTokens: {}, @@ -445,12 +450,13 @@ class FixtureBuilder { withPermissionControllerConnectedToTestDapp({ restrictReturnedAccounts = true, account = '', + useLocalhostHostname = false, } = {}) { const selectedAccount = account || DEFAULT_FIXTURE_ACCOUNT; return this.withPermissionController({ subjects: { - [DAPP_URL]: { - origin: DAPP_URL, + [useLocalhostHostname ? DAPP_URL_LOCALHOST : DAPP_URL]: { + origin: useLocalhostHostname ? DAPP_URL_LOCALHOST : DAPP_URL, permissions: { eth_accounts: { id: 'ZaqPEWxyhNCJYACFw93jE', diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts index f32a48d9c4a8..418c9d736078 100644 --- a/test/e2e/flask/btc/btc-account-overview.spec.ts +++ b/test/e2e/flask/btc/btc-account-overview.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import { Suite } from 'mocha'; import { DEFAULT_BTC_BALANCE } from '../../constants'; import { withBtcAccountSnap } from './common-btc'; @@ -46,17 +45,19 @@ describe('BTC Account - Overview', function (this: Suite) { 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(); + await driver.waitForSelector({ + testId: 'account-value-and-suffix', + text: `${DEFAULT_BTC_BALANCE}`, + }); + await driver.waitForSelector({ + css: '.currency-display-component__suffix', + text: 'BTC', + }); - const [balance, unit] = balanceText.split('\n'); - assert(Number(balance) === DEFAULT_BTC_BALANCE); - assert(unit === 'BTC'); + await driver.waitForSelector({ + tag: 'p', + text: `${DEFAULT_BTC_BALANCE} BTC`, + }); }, ); }); diff --git a/test/e2e/flask/user-operations.spec.ts b/test/e2e/flask/user-operations.spec.ts index be7141444c97..7512e0b563c9 100644 --- a/test/e2e/flask/user-operations.spec.ts +++ b/test/e2e/flask/user-operations.spec.ts @@ -256,7 +256,8 @@ describe('User Operations', function () { from: ERC_4337_ACCOUNT, to: GANACHE_ACCOUNT, value: convertETHToHexGwei(1), - data: '0x', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', }); await confirmTransaction(driver); @@ -294,7 +295,8 @@ describe('User Operations', function () { from: ERC_4337_ACCOUNT, to: GANACHE_ACCOUNT, value: convertETHToHexGwei(1), - data: '0x', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', }); await confirmTransaction(driver); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 6d2ccebeb7c7..1e03da918ed7 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -55,7 +55,7 @@ const convertETHToHexGwei = (eth) => convertToHexValue(eth * 10 ** 18); /** * * @param {object} options - * @param {(fixtures: Fixtures) => Promise} testSuite + * @param {({driver: Driver, mockedEndpoint: MockedEndpoint}: TestSuiteArguments) => Promise} testSuite */ async function withFixtures(options, testSuite) { const { @@ -65,6 +65,7 @@ async function withFixtures(options, testSuite) { smartContract, driverOptions, dappOptions, + staticServerOptions, title, ignoredConsoleErrors = [], dappPath = undefined, @@ -159,7 +160,9 @@ async function withFixtures(options, testSuite) { 'dist', ); } - dappServer.push(createStaticServer(dappDirectory)); + dappServer.push( + createStaticServer({ public: dappDirectory, ...staticServerOptions }), + ); dappServer[i].listen(`${dappBasePort + i}`); await new Promise((resolve, reject) => { dappServer[i].on('listening', resolve); @@ -382,91 +385,9 @@ const getWindowHandles = async (driver, handlesCount) => { return { extension, dapp, popup }; }; -const importSRPOnboardingFlow = async (driver, seedPhrase, password) => { - // agree to terms of use - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - - // welcome - await driver.clickElement('[data-testid="onboarding-import-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - await driver.waitForSelector('.import-srp__actions'); - // import with recovery phrase - await driver.pasteIntoField( - '[data-testid="import-srp__srp-word-0"]', - seedPhrase, - ); - await driver.clickElement('[data-testid="import-srp-confirm"]'); - - // create password - await driver.fill('[data-testid="create-password-new"]', password); - await driver.fill('[data-testid="create-password-confirm"]', password); - await driver.clickElement('[data-testid="create-password-terms"]'); - await driver.clickElement('[data-testid="create-password-import"]'); - await driver.assertElementNotPresent('.loading-overlay'); -}; - -const completeImportSRPOnboardingFlow = async ( - driver, - seedPhrase, - password, -) => { - await importSRPOnboardingFlow(driver, seedPhrase, password); - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); -}; - -const completeImportSRPOnboardingFlowWordByWord = async ( - driver, - seedPhrase, - password, -) => { - // agree to terms of use - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - - // welcome - await driver.clickElement('[data-testid="onboarding-import-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // import with recovery phrase, word by word - const words = seedPhrase.split(' '); - for (const word of words) { - await driver.pasteIntoField( - `[data-testid="import-srp__srp-word-${words.indexOf(word)}"]`, - word, - ); - } - await driver.clickElement('[data-testid="import-srp-confirm"]'); - - // create password - await driver.fill('[data-testid="create-password-new"]', password); - await driver.fill('[data-testid="create-password-confirm"]', password); - await driver.clickElement('[data-testid="create-password-terms"]'); - await driver.clickElement('[data-testid="create-password-import"]'); - - // wait for loading to complete - await driver.assertElementNotPresent('.loading-overlay'); - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); -}; - /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Begin the create new wallet flow on onboarding screen. - * * @param {WebDriver} driver */ const onboardingBeginCreateNewWallet = async (driver) => { @@ -478,8 +399,8 @@ const onboardingBeginCreateNewWallet = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Choose either "I Agree" or "No Thanks" on the MetaMetrics onboarding screen - * * @param {WebDriver} driver * @param {boolean} option - true to opt into metrics, default is false */ @@ -490,8 +411,8 @@ const onboardingChooseMetametricsOption = async (driver, option = false) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Set a password for MetaMask during onboarding - * * @param {WebDriver} driver * @param {string} password - Password to set */ @@ -504,9 +425,9 @@ const onboardingCreatePassword = async (driver, password) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Choose to secure wallet, and then get recovery phrase and confirm the SRP * during onboarding flow. - * * @param {WebDriver} driver */ const onboardingRevealAndConfirmSRP = async (driver) => { @@ -542,9 +463,9 @@ const onboardingRevealAndConfirmSRP = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Complete the onboarding flow by confirming completion. Final step before the * reminder to pin the extension. - * * @param {WebDriver} driver */ const onboardingCompleteWalletCreation = async (driver) => { @@ -554,8 +475,8 @@ const onboardingCompleteWalletCreation = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Move through the steps of pinning extension after successful onboarding - * * @param {WebDriver} driver */ const onboardingPinExtension = async (driver) => { @@ -564,9 +485,40 @@ const onboardingPinExtension = async (driver) => { await driver.clickElement('[data-testid="pin-extension-done"]'); }; -const onboardingCompleteWalletCreationWithOptOut = async (driver) => { +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * Completes the onboarding flow with optional opt-out settings for wallet creation. + * + * This function navigates through the onboarding process, allowing for opt-out of certain features. + * It waits for the appropriate heading to appear, then proceeds to opt-out of third-party API + * integration for general and assets sections if specified in the optOutOptions. + * @param {WebDriver} driver - The Selenium WebDriver instance. + * @param {object} optOutOptions - Optional. An object specifying which features to opt-out of. + * @param {boolean} optOutOptions.basicFunctionality - Optional. Defaults to true. Opt-out of basic functionality. + * @param {boolean} optOutOptions.profileSync - Optional. Defaults to true. Opt-out of profile sync. + * @param {boolean} optOutOptions.assets - Optional. Defaults to true. Opt-out of assets options. + * @param {boolean} optOutOptions.isNewWallet - Optional. Defaults to true. Indicates if this is a new wallet creation. + */ +const onboardingCompleteWalletCreationWithOptOut = async ( + driver, + optOutOptions = {}, +) => { + const defaultOptOutOptions = { + basicFunctionality: true, + profileSync: true, + assets: true, + isNewWallet: true, + }; + + const optOutOptionsToUse = { ...defaultOptOutOptions, ...optOutOptions }; + // wait for h2 to appear - await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); + await driver.findElement({ + text: optOutOptionsToUse.isNewWallet + ? 'Congratulations' + : 'Your wallet is ready', + tag: 'h2', + }); // opt-out from third party API on general section await driver.clickElementAndWaitToDisappear({ @@ -574,27 +526,47 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { 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.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( - '.toggle-button.toggle-button--on:not([data-testid="basic-functionality-toggle"] .toggle-button)', - ) - ).map((toggle) => toggle.click()), - ); + if (optOutOptionsToUse.basicFunctionality) { + await driver.clickElement( + '[data-testid="basic-functionality-toggle"] .toggle-button', + ); + await driver.clickElement('[id="basic-configuration-checkbox"]'); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Turn off', + }); + } + + if ( + optOutOptionsToUse.profileSync && + !optOutOptionsToUse.basicFunctionality + ) { + await driver.clickElement( + '[data-testid="profile-sync-toggle"] .toggle-button', + ); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Turn off', + }); + } + await driver.clickElement('[data-testid="category-back-button"]'); + if (optOutOptionsToUse.assets) { + // opt-out from third party API on assets section + await driver.clickElement({ text: 'Assets', tag: 'p' }); + await Promise.all( + ( + await driver.findClickableElements( + '.toggle-button.toggle-button--on:not([data-testid="basic-functionality-toggle"] .toggle-button)', + ) + ).map((toggle) => toggle.click()), + ); + + 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( @@ -610,17 +582,37 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await onboardingPinExtension(driver); }; +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * Completes the onboarding flow for creating a new wallet with opt-out options. + * + * This function guides the user through the onboarding process of creating a new wallet, + * including opting out of certain features as specified by the `optOutOptions` parameter. + * @param {object} driver - The Selenium driver instance. + * @param {string} password - The password to use for the new wallet. + * @param {object} optOutOptions - An object specifying the features to opt out of. + * @param {boolean} optOutOptions.isNewWallet - Indicates if this is a new wallet creation. + * @param {boolean} optOutOptions.basicFunctionality - Indicates if basic functionality should be opted out. + * @param {boolean} optOutOptions.profileSync - Indicates if profile sync should be opted out. + * @param {boolean} optOutOptions.assets - Indicates if assets should be opted out. + */ const completeCreateNewWalletOnboardingFlowWithOptOut = async ( driver, password, + optOutOptions, ) => { await onboardingBeginCreateNewWallet(driver); await onboardingChooseMetametricsOption(driver, false); await onboardingCreatePassword(driver, password); await onboardingRevealAndConfirmSRP(driver); - await onboardingCompleteWalletCreationWithOptOut(driver); + await onboardingCompleteWalletCreationWithOptOut(driver, optOutOptions); }; +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * @param driver + * @param password + */ const completeCreateNewWalletOnboardingFlow = async (driver, password) => { await onboardingBeginCreateNewWallet(driver); await onboardingChooseMetametricsOption(driver, false); @@ -630,45 +622,6 @@ const completeCreateNewWalletOnboardingFlow = async (driver, password) => { await onboardingPinExtension(driver); }; -const importWrongSRPOnboardingFlow = async (driver, seedPhrase) => { - // agree to terms of use - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - - // welcome - await driver.clickElement('[data-testid="onboarding-import-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // import with recovery phrase - await driver.pasteIntoField( - '[data-testid="import-srp__srp-word-0"]', - seedPhrase, - ); - - const warningText = 'Invalid Secret Recovery Phrase'; - const warnings = await driver.findElements('.import-srp__banner-alert-text'); - const warning = warnings[1]; - - assert.equal(await warning.getText(), warningText); -}; - -const selectDropdownByNum = async (elements, index) => { - await elements[index].click(); -}; - -const testSRPDropdownIterations = async (options, driver, iterations) => { - for (let i = 0; i < iterations; i++) { - await selectDropdownByNum(options, i); - await new Promise((resolve) => setTimeout(resolve, 1000)); - - const formFields = await driver.findElements('.import-srp__srp-word-label'); - const expectedNumFields = 12 + i * 3; - const actualNumFields = formFields.length; - assert.equal(actualNumFields, expectedNumFields); - } -}; - const openSRPRevealQuiz = async (driver) => { // navigate settings to reveal SRP await driver.clickElement('[data-testid="account-options-menu-button"]'); @@ -881,7 +834,8 @@ const sendScreenToConfirmScreen = async ( quantity, ) => { await openActionMenuAndStartSendFlow(driver); - await driver.fill('[data-testid="ens-input"]', recipientAddress); + await driver.waitForSelector('[data-testid="ens-input"]'); + await driver.pasteIntoField('[data-testid="ens-input"]', recipientAddress); await driver.fill('.unit-input__input', quantity); // check if element exists and click it @@ -900,7 +854,8 @@ const sendTransaction = async ( isAsyncFlow = false, ) => { await openActionMenuAndStartSendFlow(driver); - await driver.fill('[data-testid="ens-input"]', recipientAddress); + await driver.waitForSelector('[data-testid="ens-input"]'); + await driver.pasteIntoField('[data-testid="ens-input"]', recipientAddress); await driver.fill('.unit-input__input', quantity); await driver.clickElement({ @@ -1181,6 +1136,10 @@ async function initBundler(bundlerServer, ganacheServer, usePaymaster) { } } +/** + * @deprecated Please use page object functions in `pages/account-list-page`. + * @param driver + */ async function removeSelectedAccount(driver) { await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( @@ -1190,6 +1149,10 @@ async function removeSelectedAccount(driver) { await driver.clickElement({ text: 'Remove', tag: 'button' }); } +/** + * @deprecated Please use page object functions in `pages/account-list-page`. + * @param driver + */ async function getSelectedAccountAddress(driver) { await driver.clickElement('[data-testid="account-options-menu-button"]'); await driver.clickElement('[data-testid="account-list-menu-details"]'); @@ -1252,6 +1215,8 @@ async function openMenuSafe(driver) { } } +const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu; + module.exports = { DAPP_HOST_ADDRESS, DAPP_URL, @@ -1270,9 +1235,6 @@ module.exports = { largeDelayMs, veryLargeDelayMs, withFixtures, - importSRPOnboardingFlow, - completeImportSRPOnboardingFlow, - completeImportSRPOnboardingFlowWordByWord, completeCreateNewWalletOnboardingFlow, completeCreateNewWalletOnboardingFlowWithOptOut, openSRPRevealQuiz, @@ -1281,8 +1243,6 @@ module.exports = { closeSRPReveal, tapAndHoldToRevealSRP, createDownloadFolder, - importWrongSRPOnboardingFlow, - testSRPDropdownIterations, openDapp, openDappConnectionsPage, createDappTransaction, @@ -1313,6 +1273,7 @@ module.exports = { onboardingCreatePassword, onboardingRevealAndConfirmSRP, onboardingCompleteWalletCreation, + onboardingCompleteWalletCreationWithOptOut, onboardingPinExtension, assertInAnyOrder, genRandInitBal, @@ -1324,4 +1285,5 @@ module.exports = { getSelectedAccountAddress, tempToggleSettingRedesignedConfirmations, openMenuSafe, + sentryRegEx, }; diff --git a/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts b/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts new file mode 100644 index 000000000000..1b6591899c0e --- /dev/null +++ b/test/e2e/helpers/user-storage/userStorageMockttpController.test.ts @@ -0,0 +1,304 @@ +import * as mockttp from 'mockttp'; +import { UserStorageMockttpController } from './userStorageMockttpController'; + +describe('UserStorageMockttpController', () => { + let mockServer: mockttp.Mockttp; + + const baseUrl = 'https://user-storage.api.cx.metamask.io/api/v1/userstorage'; + + describe('mimics user storage behaviour', () => { + mockServer = mockttp.getLocal({ cors: true }); + + it('handles GET requests that have empty response', async () => { + const controller = new UserStorageMockttpController(); + + controller.setupPath('accounts', mockServer); + + const request = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(request.json).toEqual(null); + }); + + it('handles GET requests that have a pre-defined response', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const request = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(request.json).toEqual(mockedData); + }); + + it('handles batch GET requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const request = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(request.json).toEqual(mockedData); + }); + + it('handles GET requests for feature entries', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const request = await controller.onGet('accounts', { + path: `${baseUrl}/accounts/7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b`, + }); + + expect(request.json).toEqual(mockedData[0]); + }); + + it('handles PUT requests to create new entries', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + const mockedAddedData = { + HashedKey: + '6afbe024087495b4e0d56c4bdfc981c84eba44a7c284d4f455b5db4fcabc2173', + Data: 'data3', + }; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const putRequest = await controller.onPut('accounts', { + path: `${baseUrl}/accounts/6afbe024087495b4e0d56c4bdfc981c84eba44a7c284d4f455b5db4fcabc2173`, + body: { + getJson: async () => ({ + data: mockedAddedData.Data, + }), + } as unknown as mockttp.CompletedBody, + }); + + expect(putRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual([...mockedData, mockedAddedData]); + }); + + it('handles PUT requests to update existing entries', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + const mockedUpdatedData = { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data3', + }; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const putRequest = await controller.onPut('accounts', { + path: `${baseUrl}/accounts/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, + body: { + getJson: async () => ({ + data: mockedUpdatedData.Data, + }), + } as unknown as mockttp.CompletedBody, + }); + + expect(putRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual([mockedData[0], mockedUpdatedData]); + }); + + it('handles batch PUT requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + const mockedUpdatedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data3', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data4', + }, + ]; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const putData = {} as { [key: string]: string }; + mockedUpdatedData.forEach((entry) => { + putData[entry.HashedKey] = entry.Data; + }); + + const putRequest = await controller.onPut('accounts', { + path: `${baseUrl}/accounts`, + body: { + getJson: async () => ({ + data: putData, + }), + } as unknown as mockttp.CompletedBody, + }); + + expect(putRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual(mockedUpdatedData); + }); + + it('handles DELETE requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const deleteRequest = await controller.onDelete('accounts', { + path: `${baseUrl}/accounts/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, + }); + + expect(deleteRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual([mockedData[0]]); + }); + + it('handles batch DELETE requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const deleteRequest = await controller.onDelete('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(deleteRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + path: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual(null); + }); + }); +}); diff --git a/test/e2e/helpers/user-storage/userStorageMockttpController.ts b/test/e2e/helpers/user-storage/userStorageMockttpController.ts new file mode 100644 index 000000000000..970a10d11120 --- /dev/null +++ b/test/e2e/helpers/user-storage/userStorageMockttpController.ts @@ -0,0 +1,196 @@ +import { CompletedRequest, Mockttp } from 'mockttp'; + +// TODO: Export user storage schema from @metamask/profile-sync-controller +export const pathRegexps = { + accounts: + /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/accounts/u, + networks: + /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/networks/u, + notifications: + /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/notifications/u, +}; + +type UserStorageResponseData = { HashedKey: string; Data: string }; + +const determineIfFeatureEntryFromURL = (url: string) => + url.substring(url.lastIndexOf('userstorage') + 12).split('/').length === 2; + +export class UserStorageMockttpController { + paths: Map< + keyof typeof pathRegexps, + { + response: UserStorageResponseData[]; + server: Mockttp; + } + > = new Map(); + + readonly onGet = async ( + path: keyof typeof pathRegexps, + request: Pick, + statusCode: number = 200, + ) => { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return { + statusCode, + json: null, + }; + } + + const isFeatureEntry = determineIfFeatureEntryFromURL(request.path); + + if (isFeatureEntry) { + const json = + internalPathData.response?.find( + (entry) => entry.HashedKey === request.path.split('/').pop(), + ) || null; + + return { + statusCode, + json, + }; + } + + const json = internalPathData?.response.length + ? internalPathData.response + : null; + + return { + statusCode, + json, + }; + }; + + readonly onPut = async ( + path: keyof typeof pathRegexps, + request: Pick, + statusCode: number = 204, + ) => { + const isFeatureEntry = determineIfFeatureEntryFromURL(request.path); + + const data = (await request.body.getJson()) as { + data: string | { [key: string]: string }; + }; + + const newOrUpdatedSingleOrBatchEntries = + isFeatureEntry && typeof data?.data === 'string' + ? [ + { + HashedKey: request.path.split('/').pop() as string, + Data: data?.data, + }, + ] + : Object.entries(data?.data).map(([key, value]) => ({ + HashedKey: key, + Data: value, + })); + + newOrUpdatedSingleOrBatchEntries.forEach((entry) => { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return; + } + + const doesThisEntryExist = internalPathData.response?.find( + (existingEntry) => existingEntry.HashedKey === entry.HashedKey, + ); + + if (doesThisEntryExist) { + this.paths.set(path, { + ...internalPathData, + response: internalPathData.response.map((existingEntry) => + existingEntry.HashedKey === entry.HashedKey ? entry : existingEntry, + ), + }); + } else { + this.paths.set(path, { + ...internalPathData, + response: [ + ...(internalPathData?.response || []), + entry as { HashedKey: string; Data: string }, + ], + }); + } + }); + + return { + statusCode, + }; + }; + + readonly onDelete = async ( + path: keyof typeof pathRegexps, + request: Pick, + statusCode: number = 204, + ) => { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return { + statusCode, + }; + } + + const isFeatureEntry = determineIfFeatureEntryFromURL(request.path); + + if (isFeatureEntry) { + this.paths.set(path, { + ...internalPathData, + response: internalPathData?.response.filter( + (entry) => entry.HashedKey !== request.path.split('/').pop(), + ), + }); + } else { + this.paths.set(path, { + ...internalPathData, + response: [], + }); + } + + return { + statusCode, + }; + }; + + setupPath = ( + path: keyof typeof pathRegexps, + server: Mockttp, + overrides?: { + getResponse?: UserStorageResponseData[]; + getStatusCode?: number; + putStatusCode?: number; + deleteStatusCode?: number; + }, + ) => { + const previouslySetupPath = this.paths.get(path); + + this.paths.set(path, { + response: overrides?.getResponse || previouslySetupPath?.response || [], + server, + }); + + this.paths + .get(path) + ?.server.forGet(pathRegexps[path]) + .always() + .thenCallback((request) => + this.onGet(path, request, overrides?.getStatusCode), + ); + this.paths + .get(path) + ?.server.forPut(pathRegexps[path]) + .always() + .thenCallback((request) => + this.onPut(path, request, overrides?.putStatusCode), + ); + this.paths + .get(path) + ?.server.forDelete(pathRegexps[path]) + .always() + .thenCallback((request) => + this.onDelete(path, request, overrides?.deleteStatusCode), + ); + }; +} diff --git a/test/e2e/mock-cdn/cdn-config-res-headers.json b/test/e2e/mock-cdn/cdn-config-res-headers.json index 7b0e37a92449..02096db176b3 100644 --- a/test/e2e/mock-cdn/cdn-config-res-headers.json +++ b/test/e2e/mock-cdn/cdn-config-res-headers.json @@ -1,3 +1,4 @@ { - "Etag": "bb28e40153ff052671b8ad835d368d89" + "Content-Type": "text/plain", + "Etag": "\"db8ccd7f11424082a7cea67466129aed\"" } diff --git a/test/e2e/mock-cdn/cdn-config.txt b/test/e2e/mock-cdn/cdn-config.txt index b05273585ff5..edd8280e3a8e 100644 Binary files a/test/e2e/mock-cdn/cdn-config.txt and b/test/e2e/mock-cdn/cdn-config.txt differ diff --git a/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json b/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json index b3c558ff1cdd..0fb0ec0f7d89 100644 --- a/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json +++ b/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json @@ -1,3 +1,4 @@ { - "Etag": "W/\"ece7f5f533b8978063633ea5b1f8a0fc\"" + "Content-Type": "text/plain", + "Etag": "W/\"5ae8a43f84ccd89e8ddc79b1dfed0035\"" } diff --git a/test/e2e/mock-cdn/cdn-stale-diff.txt b/test/e2e/mock-cdn/cdn-stale-diff.txt index 02e2bae35ce5..44eb67f85fa4 100644 Binary files a/test/e2e/mock-cdn/cdn-stale-diff.txt and b/test/e2e/mock-cdn/cdn-stale-diff.txt differ diff --git a/test/e2e/mock-cdn/cdn-stale-res-headers.json b/test/e2e/mock-cdn/cdn-stale-res-headers.json index bb2df028661c..b9f7bf79559f 100644 --- a/test/e2e/mock-cdn/cdn-stale-res-headers.json +++ b/test/e2e/mock-cdn/cdn-stale-res-headers.json @@ -1,3 +1,4 @@ { - "Etag": "W/\"b89ab99b0801b5d64acb27893a2b31ca\"" + "Content-Type": "text/plain", + "Etag": "W/\"ab6bc9d599f83e04ae71f6ea957414f0\"" } diff --git a/test/e2e/mock-cdn/cdn-stale.txt b/test/e2e/mock-cdn/cdn-stale.txt index 39e3f2b9ea1b..42efc2a8ba97 100644 Binary files a/test/e2e/mock-cdn/cdn-stale.txt and b/test/e2e/mock-cdn/cdn-stale.txt differ diff --git a/test/e2e/mock-cdn/ppom-version-headers.json b/test/e2e/mock-cdn/ppom-version-headers.json index a29a05e8c360..ad50d161d1dd 100644 --- a/test/e2e/mock-cdn/ppom-version-headers.json +++ b/test/e2e/mock-cdn/ppom-version-headers.json @@ -1,3 +1,3 @@ { - "Etag": "W/\"9f5df4118b061a89ac013422f809de72\"" + "Etag": "W/\"7aa74f7c18a5cb2601e4fc6afcadc9cc\"" } diff --git a/test/e2e/mock-cdn/ppom-version.json b/test/e2e/mock-cdn/ppom-version.json index e06e6705218b..b529f71a0f1c 100644 --- a/test/e2e/mock-cdn/ppom-version.json +++ b/test/e2e/mock-cdn/ppom-version.json @@ -1,302 +1,512 @@ [ - { - "name": "stale", - "chainId": "0x144", - "version": "0.0.11", - "checksum": "b4731bb258fec747bf9394d4c21096dd27d498e6ada6c1a871d0407f63f9c2d3", - "signature": "49fa6b11db8114a4520343544d829753c0eedd156f15c168dd8e31a8ddc25c10c16d9203bdd5d0872610a805d7e37a26b79bf399d1c2d5037f6ebd02ac6d0306", - "hashSignature": "7a7d72a4214317738b3b91c3245a8ef8ac0f5bac247c369212c79db763ef78b1a5001f892edf41c2b619f28c326541ecd76e9bbe7db444a6d2898ab408832507", - "filePath": "stale/0x144/0.0.11", - "timestamp": "2024-03-06T11:05:22.120889" - }, - { - "name": "stale_diff", - "chainId": "0x144", - "version": "0.0.77", - "checksum": "69b726f5ae8567cd566c6bf30dea3692ffb159d1d61dcdf081298c7023635be9", - "signature": "5a5110480d0d63e35900a1e5e09d253f644a1b481c3461e0ffc8dc614dca67838cc7051e304e23ad8cf9e2b74b9e129724253da4f1239140d8474d59400b7502", - "hashSignature": "846d71a01cc094b1940cdedba0cf1e5b2d324eaca41a55d9825f759f67572f43fcc2d29bd894f66d906eebae0fe49922db5d02948748880e9ecb7a884a17b901", - "filePath": "stale_diff/0x144/0.0.77", - "timestamp": "2024-03-10T11:34:07.732508" - }, - { - "name": "config", - "chainId": "0x144", - "version": "0.0.77", - "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", - "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", - "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0x144/0.0.77", - "timestamp": "2024-03-10T11:34:07.732733" - }, { "name": "stale", "chainId": "0x38", - "version": "0.0.38", - "checksum": "99776fbf3527ad357f0772a48585750aa9dfe3ff006a4eb8788ca5cde3dfa6e9", - "signature": "e592080cca99f769f602d94b774fe7905c9e53e9be3f2bc172c0bfd9d0a641083fb735add7cfe5734d3252e217ca63a0b4fbcde6b4376c0ee6a21ce9d07ee700", - "hashSignature": "def291f8e687284afec51cfe8e4ebd9f260078f4ac0804742c31f7444a6d7ba055ab485d227707826761d8c97cba1f6bc08cb5e8fb7e9fb5fdcf1aa8190cc901", - "filePath": "stale/0x38/0.0.38", - "timestamp": "2024-03-20T21:00:30.533909" + "version": "0.0.66", + "checksum": "939cfdb932942bc70f9073fd28bbfc40d1d3d5852d83818995e9e22043c2f43f", + "signature": "8ff3884b8fd03330fc507bb71fdf979d3cdce74dc99b75e23a32e7c3df0a8ca938f6f32193690d96f97801d1aad220fcf7a2c9d4ff4e3244c4fb004cea183a02", + "hashSignature": "0076f738508122fce625dfbf5adb5f5068de6484a8bec018d0b6ce9f5818b528a42ef34905e91b30e7864384b3199058a85ef26f0a6a753127eeddad07293f0b", + "filePath": "stale/0x38/0.0.66", + "timestamp": "2024-10-27T12:35:14.835435" }, { "name": "stale", "chainId": "0x1", - "version": "0.0.52", - "checksum": "20c1449b660f32d7c8e03b0170c79878879cd9ba40fd84374bb7d86cd82a8e6b", - "signature": "9d5f4e433156ddaec3d34361b3bef4c0147c464e6810c6d5c45a0b179227f97d8ed03bc847330eb7d529cc4b5320a8a3602522820d2b44ec2928c227ed7cb700", - "hashSignature": "fd98b80b18a774c64f8bc3a48cbbd1fdd948af320dd14e229c1c2587c5ca6199cb7782989f3edc783e1534b8db2bd758b83898c12899aaf43cf319f112a07e05", - "filePath": "stale/0x1/0.0.52", - "timestamp": "2024-03-20T21:00:43.282213" + "version": "0.0.80", + "checksum": "f1750994bf1daa849d8e65b4ebfb3aca09aee65dc96343ddf96ccf9036392a10", + "signature": "1f72981666b660d281b9a92810da45ddef5828240084309fdb8dcc47ce5cdcb38a4369b18fa55331c3dddf39a47bacd9595bb868eca6fc795f11536b8adb1b08", + "hashSignature": "37fff50fd8eaf56d6a7bdc755a8dd13fc654dc9271214cdf419276ecf06b457416f17b8e76640598a3e7a3345eba3df990ddbbfbd974937912901063f22be106", + "filePath": "stale/0x1/0.0.80", + "timestamp": "2024-10-27T12:35:35.786460" }, { "name": "stale", "chainId": "0x89", - "version": "0.0.38", - "checksum": "b484bd673cd5eec0724fb22c36ba39c9ccc9721401be07fb273f27ec020bfb4a", - "signature": "7392aeb07bba7034fffe308e316f424c48bf868051b655114b63cb36038d4495d190c8daadf33b815bee9bced838aadcf2eb49cbf177d6ab38ae97b6475f7f03", - "hashSignature": "48b8e01132ffdbdd01439dd0e1d8c443bb4c2b88c29d1b9132bb107b0890d246ea73fd55ac2be3bdd0b4922400f5930d3335aafadd2141b6049f1caa1ec59d00", - "filePath": "stale/0x89/0.0.38", - "timestamp": "2024-03-20T21:00:56.806406" + "version": "0.0.66", + "checksum": "ec799a6a914e3c471edaa0d2d7a6676c32c8cd2bdb00c9ef23f6075595a39e8f", + "signature": "3718817a7eee0c190888e1046346ad865497f0785c8ff0f93fee32bb1a67a0d93890ebcf08cc2930237095b1296ff2a885d8ad6e25ac3b0803d8a879db19d10f", + "hashSignature": "00b5b427d6618c2c7b8a603e95c126715489119611266ba7c33f9cc6d3a15b86617bf11c629534d49500329eda6bc3576c688163ea838cdc48caa41921d7610b", + "filePath": "stale/0x89/0.0.66", + "timestamp": "2024-10-27T12:35:55.378375" }, { "name": "stale", "chainId": "0xa", - "version": "0.0.38", - "checksum": "7408b4f44e86e19025549c3e165f7d528f856ed3473a6efddf3d2577251d3544", - "signature": "fd0e9a82564802155a6bc13f34363dddc99ca2a3468e3f0e7b00360ee5008f6f2a30dd47771b69352fa1c4323deae756c46fc03508dc39ccccda3fb8678d7f09", - "hashSignature": "9aab8ca37a8cf0797d55c0b655e280e527751a9739766e8d2edd6c45b18dabe09f0ee66518f59a4112b45e74d5c047af7b39380a0e3f700a41d1680f24b6ad06", - "filePath": "stale/0xa/0.0.38", - "timestamp": "2024-03-20T21:01:06.639827" + "version": "0.0.66", + "checksum": "5ca4a3f1fdd546e8c5ab7553db92892bccc8e143956a922e3080455b7368ed74", + "signature": "9fd80cfb4103e55f9848ecb751596a81faba3528b3ca881f379c1919564ea8ba7ca196025dd068b8ea1aa431d61dad79a2f5edd61f8f9edd2da3fa21b4e7a902", + "hashSignature": "b5aa4508c58cbee23e8d44ca8024e4a72329de93807135c937cf8ce85ab1e8d49a8a0a6cfffaf3734a3a79ea9a57fa9448fab79987d41d58a315aeab5b7f0404", + "filePath": "stale/0xa/0.0.66", + "timestamp": "2024-10-27T12:36:13.196098" }, { "name": "stale", "chainId": "0xa4b1", - "version": "0.0.38", - "checksum": "642573df1c81669619be1bda27c1659bb730a201824f0470495d38c08acabd70", - "signature": "4d8b6c51d8f9205ce173cde3dab150ad6653f48dc2ca59c3f1beb5e574430404f8b9c03f701dc9160a56f93a35560cd57b49edef6e3f65ea71ea6bfbf21c2b0b", - "hashSignature": "4e07a1c1b15353e2a5f2c7bd35525f47cd9405f740a89a2faa5ea7096edc7278a272aed383341eaee413673a45cd8d6e042fd784493cafee2699fe01229a0b04", - "filePath": "stale/0xa4b1/0.0.38", - "timestamp": "2024-03-20T21:01:16.670454" + "version": "0.0.66", + "checksum": "878a437412a4399852866cf6f6237e1438b29d3e199ee07365541bfe0e292474", + "signature": "88a9aa564a2bc74929767aca6e3f9c118beb95790d8abb909f6bdb14a1ef83030adca0a030be5cc200fca01ea48f717c5d128deb552320a8fd7c6a1063d55c0c", + "hashSignature": "9e66daf2ecb7d0f31eeba688fe36b3a31e2076f956e9a8da30d619063905364fc61901111c70b3adc01234ecd60873edacd03e49c0f553b14ddc90686c6f350a", + "filePath": "stale/0xa4b1/0.0.66", + "timestamp": "2024-10-27T12:36:31.787509" }, { "name": "stale", "chainId": "0xa86a", - "version": "0.0.38", - "checksum": "94982df19575b44a03f3f620bb75fb673b35535e6fede1e542dfbc2166984e5c", - "signature": "d59c6d65721e7003b83870af71152559c5170fff2c81b073faf3618c344063079d2551d5d2dcd310af58a840120aa6dc1e8eba2d83c7a6eb1fd61e58999b900f", - "hashSignature": "22c6d339c1957909b68922c31c015852175e494b6db191b2d1e52612c492ec22d25dfe111eb8cd99131ae245b36aa9f9dfa989cc4d437940893c0c8d2157580a", - "filePath": "stale/0xa86a/0.0.38", - "timestamp": "2024-03-20T21:01:28.762015" + "version": "0.0.66", + "checksum": "936bb4a00c2d272fe165193dbfce43ff93a08a833607caa941079938164441c0", + "signature": "45080ba868da6561104baaf6fb6fe6b1f33c8d5c616cbb469cd09eec03bb77b55705593458d24c4dadfb08f3c4d25ba91884ded6cd2964c4f3aa97620e8f9401", + "hashSignature": "a956eb9ce828f4a3b4b3ba9bf758650a26a19f583dec81eaf69324491e4ed506f71358aced8c60f6d00fe0cb7ff4ae85395ab234cd763556b43fe66da4cd8409", + "filePath": "stale/0xa86a/0.0.66", + "timestamp": "2024-10-27T12:36:50.561386" }, { "name": "stale", "chainId": "0xe708", - "version": "0.0.28", - "checksum": "a05a57016325ea3fd2663ec61e2d2a28fff07c7cc1fd653fb0d38867b4a40b6c", - "signature": "079268869c98b0552129a9aaadb08dd4ff2cc828344365eab8bdb579f6f204cc51515d4eacc034f18fab2df64c82f7d84bec668e80a10e5b4e389eabbf8b3e03", - "hashSignature": "175b783790515ccd4446cd17c44c4877fd48a54b51d0da44fc7f861eedad275f87c425f6dcf9a1e6061c0d56eafe118e6332ce3dedf9ed4ae6951a016a392600", - "filePath": "stale/0xe708/0.0.28", - "timestamp": "2024-03-20T21:01:38.871229" + "version": "0.0.56", + "checksum": "5c04ebb4f2b6866f4bd8975bcd9b47a9ebeff17905ef0dfc3f09dcf6d91e7013", + "signature": "84a5528810b64f7e5eea57e0f831e07d2e72c3b2f2f1b694275660669bbe551f4dfe040d5de5a2f0434c4950c4cb76147c727f1bcbd299d5e687adc111c1a80d", + "hashSignature": "944cd65df550f83e9eb227d3b98c0d28cc0b415afba755106f3da85c72cde4acdfdd0d37e72b9bc261791d270cf66dd6fa5e6b463bcbf28239ff032d0edd1105", + "filePath": "stale/0xe708/0.0.56", + "timestamp": "2024-10-27T12:37:32.069927" }, { "name": "stale", "chainId": "0x2105", - "version": "0.0.16", - "checksum": "d92e7360c8504e6be5b124ba6f53030b286d64ccb050252589324ea49060ef60", - "signature": "85c7c0ad4a293e64738c722e3fe52d7d59c35c7d6cb306c54797767664aa7e47fbc9f52b4dfdf25495fe4e22acf399feacabbc8a2b9dd4eb0a0e8855ee9af607", - "hashSignature": "c93d06cea4f28a602c7871e0b946b528835900aac603187c877153dbc31aceb7fe6cdb17e03558718e62b7a001cc71aef4508098a808bc83b889e91d0fda0501", - "filePath": "stale/0x2105/0.0.16", - "timestamp": "2024-03-20T21:01:47.120043" + "version": "0.0.44", + "checksum": "254ba4141c822fda5e1fad71eb16b8280af420ed3fea70c9865d619014e2e036", + "signature": "c291aa42392ca17df1c26c301631d8be2e0d69ce3e63cf131153007b4a4f3d59b8629458b36ac1a73a5a9f129b0a1edf2861eba97c1415c5a7cb2eea5e847b03", + "hashSignature": "9eea3aa55dbbfeeadc35449781f19f3e7f52e97015d7767951c96be629a3a6f03487a87ba1993fc724f414e40bca6a4fa4f19a838328d87004214dc48f246301", + "filePath": "stale/0x2105/0.0.44", + "timestamp": "2024-10-27T12:37:11.181669" }, { "name": "stale", "chainId": "0xaa36a7", - "version": "0.0.5", - "checksum": "fa8f9b03fb688da8dc98c0e38f49f05ca1a644609742d7e2b37373d4fa56b961", - "signature": "a9473d0b8659be8332f7b2e04c530bdef5f52a24c5aeb8cdbbe8ed83daa50e97878cebd4db0280b715d8c9a4c23390e30edf2bda990a699b52dbb3514ac2e805", - "hashSignature": "a8ef8f5ccff133430cf2a660c6a9522c674cc62aade0326d71063b5d43480d05c31780cbc027e2eda281e29cf0f3b94188c9584e5e92ba21d91b9ae27056040d", - "filePath": "stale/0xaa36a7/0.0.5", - "timestamp": "2024-03-20T21:01:55.297706" + "version": "0.0.33", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0xaa36a7/0.0.33", + "timestamp": "2024-10-27T12:37:51.369806" + }, + { + "name": "stale", + "chainId": "0xcc", + "version": "0.0.26", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0xcc/0.0.26", + "timestamp": "2024-10-27T12:38:12.484316" + }, + { + "name": "stale", + "chainId": "0x0", + "version": "0.0.20", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x0/0.0.20", + "timestamp": "2024-10-27T12:38:30.230565" + }, + { + "name": "stale", + "chainId": "0x144", + "version": "0.0.33", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x144/0.0.33", + "timestamp": "2024-10-27T12:38:50.380511" + }, + { + "name": "stale", + "chainId": "0x82750", + "version": "0.0.21", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x82750/0.0.21", + "timestamp": "2024-10-27T12:39:09.335209" + }, + { + "name": "stale", + "chainId": "0x1b58", + "version": "0.0.21", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x1b58/0.0.21", + "timestamp": "2024-10-27T12:39:26.466030" + }, + { + "name": "stale", + "chainId": "0x138d5", + "version": "0.0.21", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x138d5/0.0.21", + "timestamp": "2024-10-27T12:39:42.956981" }, { "name": "stale_diff", "chainId": "0x38", - "version": "0.0.217", - "checksum": "5a73bfc69701257e3e56a8d4b47d0f17888aaa5a615ce383ad5119d6766d9133", - "signature": "59f489f4680ce4782f68e3c00a011346a366d6bd1b2e5d3de5147680317fe40d05160ffd4976b021ad89c20bc3ef4b4212a0ce70d3859dd281bdeded42204a05", - "hashSignature": "85a82f9d2adf7dd9c9a186ab936c84a71d79818f4612d8410c04670396d62118b313f9914da17bc8299c8617dcd301a38e2dafe086943f1d5eca4622a466e50c", - "filePath": "stale_diff/0x38/0.0.217", - "timestamp": "2024-03-21T12:13:14.798558" + "version": "0.0.482", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x38/0.0.482", + "timestamp": "2024-10-27T12:49:57.410484" }, { "name": "config", "chainId": "0x38", - "version": "0.0.217", - "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", - "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", - "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0x38/0.0.217", - "timestamp": "2024-03-21T12:13:14.798803" + "version": "0.0.482", + "checksum": "e29783f8be2392ee395055382aa7b19a78564f99915bb749492577712c18c6f0", + "signature": "0e4e28c1e10d8e0243fd6009787dc5742a76cc5460c2565459094aa112fe2cbddb62001b8d5771ed75e836d842f87baf2a8bdc099003797ebc6c7a80079f4701", + "hashSignature": "ac2ba4a855e4e271f1ce7f4f6b67ac1b10a56378ab3f31a638aff4d5f5ccccc9385d976eec2fb304427810995ed22bd755adac62d15d4fcf6fd4934ee3883d00", + "filePath": "config/0x38/0.0.482", + "timestamp": "2024-10-27T12:49:57.410737" }, { "name": "stale_diff", "chainId": "0x1", - "version": "0.0.260", - "checksum": "fdfde4e2d19c7cbfc307924ee49b277888bfe644dadcf2dc646e4828739adda0", - "signature": "e54483a98adf96e5f16134400a2bb7dc23693de2509a878c50e6fd0ff531f1c13e2f8fdeb58e63f5e47c2497ae5dd8c904c7456456e05bad37758a40d356710f", - "hashSignature": "7f047374e55203d2e0e52257fa9b8fa2c455e27a01f131956063156335476bd0c925a8f60506820861b4a18335835bdddf1be99486aa45df331514d72b22aa0c", - "filePath": "stale_diff/0x1/0.0.260", - "timestamp": "2024-03-21T12:13:18.827919" + "version": "0.0.525", + "checksum": "ae3059765d220e8cda72aa43638916b9baac84f264a39a1d147a5b144378df62", + "signature": "3ed03cfa8bee704816a9ceb270fda86d7b007f0fe510d6acc40f96b15819c114fbd768d8845d75ab803c981eb151b4b0a24af705a27e1f96381bdc6dc5e3b50f", + "hashSignature": "9394bc16a948ab41ee74c0270a900092cbb8707fe72d3576fd75f0b87c698089c0a10b45a20ea47691a90cee427e605f81838b87424291902a9b54fec19e0709", + "filePath": "stale_diff/0x1/0.0.525", + "timestamp": "2024-10-27T12:50:03.871663" }, { "name": "config", "chainId": "0x1", - "version": "0.0.260", - "checksum": "29771bc6544e0d66898eb727ed1b4db478b33e8e45be560de84880c2433ebca2", - "signature": "1a501372b5bd9ac95accd6bf8caeec08425f3e826f100e3ca9df1dff8a861d713207e387676ed64df920ac4682888da76bde534157d71ec270e28e66b033290e", - "hashSignature": "a1cb93bea92cfbe79dd2d9023e0f7b748f0f370c97f2eabdb00a215b39dcac7a32614aa2729dcefe2a7c57a6bce78c934187a3ea443944b13b4da2fa7ee5ac0a", - "filePath": "config/0x1/0.0.260", - "timestamp": "2024-03-21T12:13:18.828225" + "version": "0.0.525", + "checksum": "abe69e1c8f6084d26b1d556bb3ae4919628ac4bf3b468bea95e3c14254bdf290", + "signature": "52ffaf9e1a543f8164ea93558467f7f4e02c15650daf92f1a1e374848c53b91dcca96037fd6d7bd63b13e7fcf88a1bcc9fe7c7915d8d6949bd153e6bf6b1a403", + "hashSignature": "83c1edb28635049e4c99d8610782506818ef071de95df49f9242f229991924b4ea829782b0ac125de3f763fc7415baaebf3732a920fb4d01861e1fdd5cb86207", + "filePath": "config/0x1/0.0.525", + "timestamp": "2024-10-27T12:50:03.871914" }, { "name": "stale_diff", "chainId": "0x89", - "version": "0.0.216", - "checksum": "d38399d82b0615fbada643872c7dfc7debf183cc1634d643ce608f8c8ffc5d20", - "signature": "89506ef81561309831f4a27cac0d330c8d14607fd646906391c73fccecb4b399931b81e29c2358747414fccee9cb774d936c8d32d8b55f3f3f0adca750f8e805", - "hashSignature": "72cdb5e05f4b1da1ed80140457e7d130beaf8eb6be864847e01befecc6bd619db5ff924578d4b127f82548766ee4b7ac49256026b2012f923fceb0bef0e6300e", - "filePath": "stale_diff/0x89/0.0.216", - "timestamp": "2024-03-21T12:13:22.608506" + "version": "0.0.481", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x89/0.0.481", + "timestamp": "2024-10-27T12:50:09.561729" }, { "name": "config", "chainId": "0x89", - "version": "0.0.216", - "checksum": "d75160f71081e7fe387344f51fb48251b8e7a91e7690be97ee845967494dfd86", - "signature": "78a82de98e2ac84c47d392296625679504f327263b3ace3b96686be3a443b76a0ae4e5cfb38962d3024e73250fe8479b423af21cc28b09defbc20f0285d60e04", - "hashSignature": "16840dddb6f35de35dd57159056217d947d7bc242cedc60eff3ccad664ed952f2287f19ee6a0413c929299afcd04447c217494c9bb2c433879f6c2ea66e69c03", - "filePath": "config/0x89/0.0.216", - "timestamp": "2024-03-21T12:13:22.608797" + "version": "0.0.481", + "checksum": "eb3b41ae8c3bbf9595dcded8e1b9090c8ed2427e236375652f6fd701c6e134b4", + "signature": "79552c7fa525e05c9c086fe8d8ecb49375be796176877e17332fa1137d4c653f224873bdac2b6fd4fe63fcfe6d404778684116e98fdb0563f63ae1efcfafe60d", + "hashSignature": "9c8a84e430578290eceea60ba7dec3695e0cfd343f7ac3e2667c8634afa3dd65d6ec25b112ac8260dbe3e6bc9e00ba906c4212975652354b1423f6ee0ec7b20e", + "filePath": "config/0x89/0.0.481", + "timestamp": "2024-10-27T12:50:09.561989" }, { "name": "stale_diff", "chainId": "0xa", - "version": "0.0.216", - "checksum": "4d44ce4c9c9e8b12ee5cc696b21f6409429d9f55cdc703c5acf799836c80ad8a", - "signature": "679813ccd33b254e88c19421e95a93e16c1db4fa473b8f9a510df7fe9ca56c7c0f21b303660d2e1175b1dc47c40aaa4fa989e710cfc2a432ad65cb7d0a522c0a", - "hashSignature": "ab86ad1046968599d8d85e0417e504c05f8bd87051ab4edee46a7946699f21e827bce2093dc94aee7f6746bb25413f819b4ca19950022fa0f8ed80efde0d2902", - "filePath": "stale_diff/0xa/0.0.216", - "timestamp": "2024-03-21T12:13:26.693599" + "version": "0.0.481", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0xa/0.0.481", + "timestamp": "2024-10-27T12:50:15.189820" }, { "name": "config", "chainId": "0xa", - "version": "0.0.216", - "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", - "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", - "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0xa/0.0.216", - "timestamp": "2024-03-21T12:13:26.693846" + "version": "0.0.481", + "checksum": "eb3b41ae8c3bbf9595dcded8e1b9090c8ed2427e236375652f6fd701c6e134b4", + "signature": "79552c7fa525e05c9c086fe8d8ecb49375be796176877e17332fa1137d4c653f224873bdac2b6fd4fe63fcfe6d404778684116e98fdb0563f63ae1efcfafe60d", + "hashSignature": "9c8a84e430578290eceea60ba7dec3695e0cfd343f7ac3e2667c8634afa3dd65d6ec25b112ac8260dbe3e6bc9e00ba906c4212975652354b1423f6ee0ec7b20e", + "filePath": "config/0xa/0.0.481", + "timestamp": "2024-10-27T12:50:15.190077" }, { "name": "stale_diff", "chainId": "0xa4b1", - "version": "0.0.216", - "checksum": "837ea3eef03c7a5a9975e5435581d6d051c5b4bd9a60e726ab044c8f7d911c7c", - "signature": "1c8252591e29761981ca792e2fb5e8c611906a7a136f8ac09e5fe0290ac3106cf425f0d38bc579739f09dba6ea4b9de50a77202f6d11e6b24e1f4d242af83603", - "hashSignature": "ffb5d7f443682e36b2928bccfe8919ecfb591b2cbf8cda82440332aec5927d39a3521c93fb7a39db20516a70a5dc101fddbc4548dabd1ee221fc754ca4c5040c", - "filePath": "stale_diff/0xa4b1/0.0.216", - "timestamp": "2024-03-21T12:13:31.844985" + "version": "0.0.481", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0xa4b1/0.0.481", + "timestamp": "2024-10-27T12:50:21.791169" }, { "name": "config", "chainId": "0xa4b1", - "version": "0.0.216", - "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", - "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", - "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0xa4b1/0.0.216", - "timestamp": "2024-03-21T12:13:31.845230" + "version": "0.0.481", + "checksum": "eb3b41ae8c3bbf9595dcded8e1b9090c8ed2427e236375652f6fd701c6e134b4", + "signature": "79552c7fa525e05c9c086fe8d8ecb49375be796176877e17332fa1137d4c653f224873bdac2b6fd4fe63fcfe6d404778684116e98fdb0563f63ae1efcfafe60d", + "hashSignature": "9c8a84e430578290eceea60ba7dec3695e0cfd343f7ac3e2667c8634afa3dd65d6ec25b112ac8260dbe3e6bc9e00ba906c4212975652354b1423f6ee0ec7b20e", + "filePath": "config/0xa4b1/0.0.481", + "timestamp": "2024-10-27T12:50:21.791427" }, { "name": "stale_diff", "chainId": "0xa86a", - "version": "0.0.216", - "checksum": "a4b8a87b84c93d03c1e51e83046edcda608be70d32924bad82aa3f93a0633f0c", - "signature": "be4cf087a3491184a4702cb9d4368775df68651735e050e2f2b16845376b87986ed24b00cde91a0d1a5739ddde513d9c9e97949b26ce96cb71b657c7e4d24a04", - "hashSignature": "0a74a14fa27289d347db514bfd11d2edb281cc6cf1bd7917705430dd7b4d97b245daa78bdcc07b74bd519619518432c7658d9c3e17b7def5e948655c9222f606", - "filePath": "stale_diff/0xa86a/0.0.216", - "timestamp": "2024-03-21T12:13:36.520723" + "version": "0.0.481", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0xa86a/0.0.481", + "timestamp": "2024-10-27T12:50:27.602732" }, { "name": "config", "chainId": "0xa86a", - "version": "0.0.216", + "version": "0.0.481", "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0xa86a/0.0.216", - "timestamp": "2024-03-21T12:13:36.520972" + "filePath": "config/0xa86a/0.0.481", + "timestamp": "2024-10-27T12:50:27.602991" }, { "name": "stale_diff", "chainId": "0xe708", - "version": "0.0.170", - "checksum": "79ed396331e95cfb34b5e14fce09b23c2059d78ada026cc38dc8f1119b793cee", - "signature": "8ae4be3b31e45eab20c4c0a177cd68ccba4293f790702392da02f840398e8da3525e69fb129c3c600c28c6dd773abfbe368a9b02c37076e82a67a906fc712e09", - "hashSignature": "9b3bacc5d3e9c50e711b8b0fd9e50dde38cc93c0fcde468efe8cac03e3b3b3fe818cfa105029b0fcac8ecc9c03c4c4833ad5da0fce9ed2b80e9d762640a2410b", - "filePath": "stale_diff/0xe708/0.0.170", - "timestamp": "2024-03-21T12:13:41.925924" + "version": "0.0.435", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0xe708/0.0.435", + "timestamp": "2024-10-27T12:50:39.077328" }, { "name": "config", "chainId": "0xe708", - "version": "0.0.170", + "version": "0.0.435", "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0xe708/0.0.170", - "timestamp": "2024-03-21T12:13:41.926309" + "filePath": "config/0xe708/0.0.435", + "timestamp": "2024-10-27T12:50:39.077595" }, { "name": "stale_diff", "chainId": "0x2105", - "version": "0.0.105", - "checksum": "e9135ad2c08b3b0349db7e2decb871598251680d81c320718ac59de91b0cfcc8", - "signature": "e47d5f9afdd1c0557cdc020692d80bc844477c8407392ab860ce8b1efb95e2c303695699d6a464dfe071685d87990bbfceb1ab166bffff795c883444543b9404", - "hashSignature": "58224a7946eea888848a9305189c080f4eab04aca4be0bd69b08a799c7cb3c8595dcd354e021fccfd4c304465d94819dd1978e8dd898938d1391dfb508735e08", - "filePath": "stale_diff/0x2105/0.0.105", - "timestamp": "2024-03-21T12:13:47.580538" + "version": "0.0.370", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x2105/0.0.370", + "timestamp": "2024-10-27T12:50:33.715820" }, { "name": "config", "chainId": "0x2105", - "version": "0.0.105", + "version": "0.0.370", + "checksum": "eb3b41ae8c3bbf9595dcded8e1b9090c8ed2427e236375652f6fd701c6e134b4", + "signature": "79552c7fa525e05c9c086fe8d8ecb49375be796176877e17332fa1137d4c653f224873bdac2b6fd4fe63fcfe6d404778684116e98fdb0563f63ae1efcfafe60d", + "hashSignature": "9c8a84e430578290eceea60ba7dec3695e0cfd343f7ac3e2667c8634afa3dd65d6ec25b112ac8260dbe3e6bc9e00ba906c4212975652354b1423f6ee0ec7b20e", + "filePath": "config/0x2105/0.0.370", + "timestamp": "2024-10-27T12:50:33.716077" + }, + { + "name": "stale_diff", + "chainId": "0xaa36a7", + "version": "0.0.289", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0xaa36a7/0.0.289", + "timestamp": "2024-10-27T12:50:44.187191" + }, + { + "name": "config", + "chainId": "0xaa36a7", + "version": "0.0.289", "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0x2105/0.0.105", - "timestamp": "2024-03-21T12:13:47.580833" + "filePath": "config/0xaa36a7/0.0.289", + "timestamp": "2024-10-27T12:50:44.187491" }, { "name": "stale_diff", - "chainId": "0xaa36a7", - "version": "0.0.24", - "checksum": "501048a17060b390dd5f6d3556dfae356299f2e1761170c5e77f71ae304b38a5", - "signature": "453422760071953f014675c10e7b540474847a7901d078aa892c1ea6cde2f669209772042b73573943cb3b123ae4ea4c48b3bda285a13262ec93e4acffe51e07", - "hashSignature": "4e1a3c0e259dfa4ee5bdfc9580a3d53c817229bb076b2ff90ad63cf7db5444073e1257a27f8be0cce7825f8824fe6a3f698c77f4f24d156b52d019bec155460c", - "filePath": "stale_diff/0xaa36a7/0.0.24", - "timestamp": "2024-03-21T12:13:51.767514" + "chainId": "0xcc", + "version": "0.0.238", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0xcc/0.0.238", + "timestamp": "2024-10-27T12:50:49.423599" }, { "name": "config", - "chainId": "0xaa36a7", - "version": "0.0.24", + "chainId": "0xcc", + "version": "0.0.238", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0xcc/0.0.238", + "timestamp": "2024-10-27T12:50:49.423903" + }, + { + "name": "stale_diff", + "chainId": "0x0", + "version": "0.0.179", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x0/0.0.179", + "timestamp": "2024-10-27T12:50:57.113651" + }, + { + "name": "config", + "chainId": "0x0", + "version": "0.0.179", "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", - "filePath": "config/0xaa36a7/0.0.24", - "timestamp": "2024-03-21T12:13:51.767758" + "filePath": "config/0x0/0.0.179", + "timestamp": "2024-10-27T12:50:57.113910" + }, + { + "name": "stale_diff", + "chainId": "0x144", + "version": "0.0.258", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x144/0.0.258", + "timestamp": "2024-10-27T12:51:10.842031" + }, + { + "name": "config", + "chainId": "0x144", + "version": "0.0.258", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0x144/0.0.258", + "timestamp": "2024-10-27T12:51:10.842292" + }, + { + "name": "stale_diff", + "chainId": "0x82750", + "version": "0.0.180", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x82750/0.0.180", + "timestamp": "2024-10-27T12:51:16.756456" + }, + { + "name": "config", + "chainId": "0x82750", + "version": "0.0.180", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0x82750/0.0.180", + "timestamp": "2024-10-27T12:51:16.756829" + }, + { + "name": "stale_diff", + "chainId": "0x1b58", + "version": "0.0.180", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x1b58/0.0.180", + "timestamp": "2024-10-27T12:51:23.355191" + }, + { + "name": "config", + "chainId": "0x1b58", + "version": "0.0.180", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0x1b58/0.0.180", + "timestamp": "2024-10-27T12:51:23.355470" + }, + { + "name": "stale_diff", + "chainId": "0x138d5", + "version": "0.0.180", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x138d5/0.0.180", + "timestamp": "2024-10-27T12:51:29.701505" + }, + { + "name": "config", + "chainId": "0x138d5", + "version": "0.0.180", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0x138d5/0.0.180", + "timestamp": "2024-10-27T12:51:29.701771" + }, + { + "name": "stale", + "chainId": "0x1b6e6", + "version": "0.0.13", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x1b6e6/0.0.13", + "timestamp": "2024-10-27T12:40:16.522556" + }, + { + "name": "stale_diff", + "chainId": "0x1b6e6", + "version": "0.0.114", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x1b6e6/0.0.114", + "timestamp": "2024-10-27T12:51:43.249992" + }, + { + "name": "config", + "chainId": "0x1b6e6", + "version": "0.0.114", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0x1b6e6/0.0.114", + "timestamp": "2024-10-27T12:51:43.250277" + }, + { + "name": "stale_diff", + "chainId": "0x138d4", + "version": "0.0.75", + "checksum": "3bab009ec87420bb6de56069cb3b76614c891688beb233bfe408aa60757478fb", + "signature": "481f19700f7399df4eefc8162e0229ea2a81fbfcbd6c327fac4ad79aaa7ac3aa8215b36bbdc74986c008cd60ffec428aa7ea04cd166faad938e1f9f4cef4b804", + "hashSignature": "f4991d29cefb09343ed5b3332416e2bab7428499373ea4bb0f6dd9bc1955eed33f80f383f70350a74ccddbea07e52eb4a144559626214a6c0f8c726b28be2f03", + "filePath": "stale_diff/0x138d4/0.0.75", + "timestamp": "2024-10-27T12:51:36.220430" + }, + { + "name": "config", + "chainId": "0x138d4", + "version": "0.0.75", + "checksum": "3e1772693c4e2fa91ae00c4b79d546f36e97525daa60baab372c145e979e19f4", + "signature": "f56c9387e07892aceb1018db6dc7e32cce0528c131b6ac1822e12e87dd43d40fdb9f3e17a9c6d7fd25a095e000db19acaf3f93c42142c48f35b06f07998f0f0e", + "hashSignature": "ae3fbbe3f87e48e537e9c1c9601fa5706ed72145250e5f4a2d7a4902d59e2c770c7c1ef88bde15505fa6bcbf24f85ef1b5ad48b76447a3479f7f7a462f082302", + "filePath": "config/0x138d4/0.0.75", + "timestamp": "2024-10-27T12:51:36.220693" + }, + { + "name": "stale", + "chainId": "0x138d4", + "version": "0.0.7", + "checksum": "f01b489470ec41ece8b65ee23e2b092e77046eb9f21a54a9c39ef2399ecc721f", + "signature": "cf94226430160273f94699041b56e9edfc603023ba4b653cf9c555a9f21065078774e1d554eae602d4a5e7bba02e0a76047a42e9a0a89acb1c2069a28c9f6f02", + "hashSignature": "939dd7022011d78cfd935ce88ca041660df72480ce8d9ec84a66066e54e030eb4abb2beadaad8ead4db192c1d0558ac1faa6703b8dda2101481accaf5f6f3005", + "filePath": "stale/0x138d4/0.0.7", + "timestamp": "2024-10-27T12:40:00.249463" } ] diff --git a/test/e2e/mock-cdn/update-mock-cdn-files.js b/test/e2e/mock-cdn/update-mock-cdn-files.js index 5fa2d7cc51a5..139c60c23cef 100644 --- a/test/e2e/mock-cdn/update-mock-cdn-files.js +++ b/test/e2e/mock-cdn/update-mock-cdn-files.js @@ -65,6 +65,11 @@ async function updateMockCdnFiles() { const { mainnetConfigVersion, mainnetStaleVersion, mainnetStaleDiffVersion } = await getFileVersions(); + // Function to create header object with Etag and Content-Type + const createHeaderObject = (etag) => ({ + Etag: etag, + 'Content-Type': 'text/plain', + }); // updating cdn-config-res-headers.json file const configResponse = await fetch( `${PPOM_CONFIG_URL}${mainnetConfigVersion}`, @@ -76,7 +81,7 @@ async function updateMockCdnFiles() { const configHeaders = configResponse.headers; const etagConfig = configHeaders.get('etag'); - const etagConfigObject = { Etag: etagConfig }; + const etagConfigObject = createHeaderObject(etagConfig); writeFileSync( `${MOCK_CDN_FOLDER_URL}cdn-config-res-headers.json`, @@ -91,7 +96,7 @@ async function updateMockCdnFiles() { const staleHeaders = staleResponse.headers; const etagStale = staleHeaders.get('etag'); - const etagStaleObject = { Etag: etagStale }; + const etagStaleObject = createHeaderObject(etagStale); writeFileSync( `${MOCK_CDN_FOLDER_URL}cdn-stale-res-headers.json`, @@ -109,7 +114,7 @@ async function updateMockCdnFiles() { const staleDiffHeaders = staleDiffResponse.headers; const etagStaleDiff = staleDiffHeaders.get('etag'); - const etagStaleDiffObject = { Etag: etagStaleDiff }; + const etagStaleDiffObject = createHeaderObject(etagStaleDiff); writeFileSync( `${MOCK_CDN_FOLDER_URL}cdn-stale-diff-res-headers.json`, diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 12d0fb293e15..5d372f3389a3 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -1,5 +1,8 @@ const fs = require('fs'); +const { + SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST, +} = require('../../shared/constants/security-provider'); const { BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, @@ -13,6 +16,7 @@ const { SWAPS_API_V2_BASE_URL, TOKEN_API_BASE_URL, } = require('../../shared/constants/swaps'); +const { SECURITY_ALERTS_PROD_API_BASE_URL } = require('./tests/ppom/constants'); const { DEFAULT_FEATURE_FLAGS_RESPONSE: BRIDGE_DEFAULT_FEATURE_FLAGS_RESPONSE, } = require('./tests/bridge/constants'); @@ -103,7 +107,7 @@ const privateHostMatchers = [ async function setupMocking( server, testSpecificMock, - { chainId, ethConversionInUsd = '1700' }, + { chainId, ethConversionInUsd = 1700 }, ) { const privacyReport = new Set(); await server.forAnyRequest().thenPassThrough({ @@ -151,6 +155,30 @@ async function setupMocking( }; }); + await server + .forGet(`${SECURITY_ALERTS_PROD_API_BASE_URL}/supportedChains`) + .thenCallback(() => { + return { + statusCode: 200, + json: SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST, + }; + }); + + await server + .forPost(`${SECURITY_ALERTS_PROD_API_BASE_URL}/validate/${chainId}`) + .thenCallback(() => { + return { + statusCode: 200, + json: { + block: 20733513, + result_type: 'Benign', + reason: '', + description: '', + features: [], + }, + }; + }); + await server .forPost( 'https://arbitrum-mainnet.infura.io/v3/00000000000000000000000000000000', @@ -588,13 +616,15 @@ async function setupMocking( }); await server - .forGet('https://min-api.cryptocompare.com/data/price') - .withQuery({ fsym: 'ETH', tsyms: 'USD' }) + .forGet('https://min-api.cryptocompare.com/data/pricemulti') + .withQuery({ fsyms: 'ETH', tsyms: 'usd' }) .thenCallback(() => { return { statusCode: 200, json: { - USD: ethConversionInUsd, + ETH: { + USD: ethConversionInUsd, + }, }, }; }); diff --git a/test/e2e/page-objects/flows/login.flow.ts b/test/e2e/page-objects/flows/login.flow.ts index 87239e3f19f1..fcd0bcb22d8a 100644 --- a/test/e2e/page-objects/flows/login.flow.ts +++ b/test/e2e/page-objects/flows/login.flow.ts @@ -18,10 +18,6 @@ export const loginWithoutBalanceValidation = async ( const loginPage = new LoginPage(driver); await loginPage.check_pageIsLoaded(); await loginPage.loginToHomepage(password); - - // user should land on homepage after successfully logging in with password - const homePage = new HomePage(driver); - await homePage.check_pageIsLoaded(); }; /** @@ -37,10 +33,14 @@ export const loginWithBalanceValidation = async ( password?: string, ) => { await loginWithoutBalanceValidation(driver, password); + // user should land on homepage after successfully logging in with password + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + // Verify the expected balance on the homepage if (ganacheServer) { - await new HomePage(driver).check_ganacheBalanceIsDisplayed(ganacheServer); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); } else { - await new HomePage(driver).check_expectedBalanceIsDisplayed(); + await homePage.check_expectedBalanceIsDisplayed(); } }; diff --git a/test/e2e/page-objects/flows/network.flow.ts b/test/e2e/page-objects/flows/network.flow.ts new file mode 100644 index 000000000000..fc77db8895bd --- /dev/null +++ b/test/e2e/page-objects/flows/network.flow.ts @@ -0,0 +1,60 @@ +import { Driver } from '../../webdriver/driver'; +import HeaderNavbar from '../pages/header-navbar'; +import SelectNetwork from '../pages/dialog/select-network'; +import NetworkSwitchModalConfirmation from '../pages/dialog/network-switch-modal-confirmation'; + +/** + * Switches to a specified network in the header bar. + * + * @param driver + * @param networkName - The name of the network to switch to. + * @param toggleShowTestNetwork - A boolean indicating whether to toggle the display of test networks. Defaults to false. + */ +export const switchToNetworkFlow = async ( + driver: Driver, + networkName: string, + toggleShowTestNetwork: boolean = false, +) => { + console.log(`Switch to network ${networkName} in header bar`); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.clickSwitchNetworkDropDown(); + + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + if (toggleShowTestNetwork) { + await selectNetworkDialog.toggleShowTestNetwork(); + } + await selectNetworkDialog.selectNetworkName(networkName); + await headerNavbar.check_currentSelectedNetwork(networkName); +}; + +/** + * Search for a network in the select network dialog and switches to it. + * + * @param driver + * @param networkName - The name of the network to search for and switch to. + */ +export const searchAndSwitchToNetworkFlow = async ( + driver: Driver, + networkName: string, +) => { + console.log( + `Search in select network dialog and switch to network ${networkName}`, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_pageIsLoaded(); + await headerNavbar.clickSwitchNetworkDropDown(); + + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.fillNetworkSearchInput(networkName); + await selectNetworkDialog.clickAddButton(); + + const networkSwitchModalConfirmation = new NetworkSwitchModalConfirmation( + driver, + ); + await networkSwitchModalConfirmation.check_pageIsLoaded(); + await networkSwitchModalConfirmation.clickApproveButton(); + await headerNavbar.check_currentSelectedNetwork(networkName); +}; diff --git a/test/e2e/page-objects/flows/onboarding.flow.ts b/test/e2e/page-objects/flows/onboarding.flow.ts new file mode 100644 index 000000000000..e235048b0a01 --- /dev/null +++ b/test/e2e/page-objects/flows/onboarding.flow.ts @@ -0,0 +1,138 @@ +import { Driver } from '../../webdriver/driver'; +import OnboardingMetricsPage from '../pages/onboarding/onboarding-metrics-page'; +import OnboardingPasswordPage from '../pages/onboarding/onboarding-password-page'; +import OnboardingSrpPage from '../pages/onboarding/onboarding-srp-page'; +import StartOnboardingPage from '../pages/onboarding/start-onboarding-page'; +import SecureWalletPage from '../pages/onboarding/secure-wallet-page'; +import OnboardingCompletePage from '../pages/onboarding/onboarding-complete-page'; +import { WALLET_PASSWORD } from '../../helpers'; +import { E2E_SRP } from '../../default-fixture'; + +/** + * Create new wallet onboarding flow + * + * @param driver - The WebDriver instance. + * @param password - The password to create. Defaults to WALLET_PASSWORD. + */ +export const createNewWalletOnboardingFlow = async ( + driver: Driver, + password: string = WALLET_PASSWORD, +) => { + console.log('Starting the creation of a new wallet onboarding flow'); + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickCreateWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.createWalletPassword(password); + + const secureWalletPage = new SecureWalletPage(driver); + await secureWalletPage.check_pageIsLoaded(); + await secureWalletPage.revealAndConfirmSRP(); +}; + +/** + * Import SRP onboarding flow + * + * @param options - The options object. + * @param options.driver - The WebDriver instance. + * @param [options.seedPhrase] - The seed phrase to import. Defaults to E2E_SRP. + * @param [options.password] - The password to use. Defaults to WALLET_PASSWORD. + * @param [options.fillSrpWordByWord] - Whether to fill the SRP word by word. Defaults to false. + */ +export const importSRPOnboardingFlow = async ({ + driver, + seedPhrase = E2E_SRP, + password = WALLET_PASSWORD, + fillSrpWordByWord = false, +}: { + driver: Driver; + seedPhrase?: string; + password?: string; + fillSrpWordByWord?: boolean; +}): Promise => { + console.log('Starting the import of SRP onboarding flow'); + await driver.navigate(); + + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickImportWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingSrpPage = new OnboardingSrpPage(driver); + await onboardingSrpPage.check_pageIsLoaded(); + if (fillSrpWordByWord) { + await onboardingSrpPage.fillSrpWordByWord(seedPhrase); + } else { + await onboardingSrpPage.fillSrp(seedPhrase); + } + await onboardingSrpPage.clickConfirmButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.createImportedWalletPassword(password); +}; + +/** + * Complete create new wallet onboarding flow + * + * @param driver - The WebDriver instance. + * @param password - The password to use. Defaults to WALLET_PASSWORD. + */ +export const completeCreateNewWalletOnboardingFlow = async ( + driver: Driver, + password: string = WALLET_PASSWORD, +) => { + console.log('start to complete create new wallet onboarding flow '); + await createNewWalletOnboardingFlow(driver, password); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_congratulationsMessageIsDisplayed(); + await onboardingCompletePage.completeOnboarding(); +}; + +/** + * Complete import SRP onboarding flow + * + * @param options - The options object. + * @param options.driver - The WebDriver instance. + * @param [options.seedPhrase] - The seed phrase to import. Defaults to E2E_SRP. + * @param [options.password] - The password to use. Defaults to WALLET_PASSWORD. + * @param [options.fillSrpWordByWord] - Whether to fill the SRP word by word. Defaults to false. + * @returns A promise that resolves when the onboarding flow is complete. + */ +export const completeImportSRPOnboardingFlow = async ({ + driver, + seedPhrase = E2E_SRP, + password = WALLET_PASSWORD, + fillSrpWordByWord = false, +}: { + driver: Driver; + seedPhrase?: string; + password?: string; + fillSrpWordByWord?: boolean; +}): Promise => { + console.log('Starting to complete import SRP onboarding flow'); + await importSRPOnboardingFlow({ + driver, + seedPhrase, + password, + fillSrpWordByWord, + }); + + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.completeOnboarding(); +}; diff --git a/test/e2e/page-objects/flows/send-transaction.flow.ts b/test/e2e/page-objects/flows/send-transaction.flow.ts index 1cffe2428873..3f56076e3934 100644 --- a/test/e2e/page-objects/flows/send-transaction.flow.ts +++ b/test/e2e/page-objects/flows/send-transaction.flow.ts @@ -7,19 +7,26 @@ import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; /** * This function initiates the steps required to send a transaction from the 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 params - An object containing the parameters. + * @param params.driver - The webdriver instance. + * @param params.recipientAddress - The recipient address. + * @param params.amount - The amount of the asset to be sent in the transaction. + * @param params.gasFee - The expected transaction gas fee. + * @param params.totalFee - The expected total transaction fee. */ -export const sendTransaction = async ( - driver: Driver, - recipientAddress: string, - amount: string, - gasfee: string, - totalfee: string, -): Promise => { +export const sendTransactionToAddress = async ({ + driver, + recipientAddress, + amount, + gasFee, + totalFee, +}: { + driver: Driver; + recipientAddress: string; + amount: string; + gasFee: string; + totalFee: string; +}): Promise => { console.log( `Start flow to send amount ${amount} to recipient ${recipientAddress} on home screen`, ); @@ -36,31 +43,89 @@ export const sendTransaction = async ( // confirm transaction when user lands on confirm transaction screen const confirmTxPage = new ConfirmTxPage(driver); - await confirmTxPage.check_pageIsLoaded(gasfee, totalfee); + await confirmTxPage.check_pageIsLoaded(gasFee, totalFee); + await confirmTxPage.confirmTx(); +}; + +/** + * This function initiates the steps required to send a transaction from the homepage to final confirmation. + * + * @param params - An object containing the parameters. + * @param params.driver - The webdriver instance. + * @param params.recipientAccount - The recipient account. + * @param params.amount - The amount of the asset to be sent in the transaction. + * @param params.gasFee - The expected transaction gas fee. + * @param params.totalFee - The expected total transaction fee. + */ +export const sendTransactionToAccount = async ({ + driver, + recipientAccount, + amount, + gasFee, + totalFee, +}: { + driver: Driver; + recipientAccount: string; + amount: string; + gasFee: string; + totalFee: string; +}): Promise => { + console.log( + `Start flow to send amount ${amount} to recipient account ${recipientAccount} on home screen`, + ); + // click send button on homepage to start flow + const homePage = new HomePage(driver); + await homePage.startSendFlow(); + + // user should land on send token screen to fill recipient and amount + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.selectRecipientAccount(recipientAccount); + await sendToPage.fillAmount(amount); + await sendToPage.goToNextScreen(); + + // confirm transaction when user lands on confirm transaction screen + const confirmTxPage = new ConfirmTxPage(driver); + await confirmTxPage.check_pageIsLoaded(gasFee, totalFee); await confirmTxPage.confirmTx(); }; /** * 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. + * @param params - An object containing the parameters. + * @param params.driver - The webdriver instance. + * @param params.recipientAddress - The recipient address. + * @param params.amount - The amount of the asset to be sent in the transaction. + * @param params.gasFee - The expected transaction gas fee. + * @param params.totalFee - The expected total transaction fee. + * @param params.isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param params.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); +export const sendTransactionWithSnapAccount = async ({ + driver, + recipientAddress, + amount, + gasFee, + totalFee, + isSyncFlow = true, + approveTransaction = true, +}: { + driver: Driver; + recipientAddress: string; + amount: string; + gasFee: string; + totalFee: string; + isSyncFlow?: boolean; + approveTransaction?: boolean; +}): Promise => { + await sendTransactionToAddress({ + driver, + recipientAddress, + amount, + gasFee, + totalFee, + }); if (!isSyncFlow) { await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( approveTransaction, diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 7218c727a929..fb7c1232c08c 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,4 +1,5 @@ import { Driver } from '../../webdriver/driver'; +import { largeDelayMs } from '../../helpers'; class AccountListPage { private readonly driver: Driver; @@ -17,12 +18,19 @@ class AccountListPage { private readonly accountOptionsMenuButton = '[data-testid="account-list-item-menu-button"]'; + private readonly accountQrCodeImage = '.qr-code__wrapper'; + + private readonly accountQrCodeAddress = '.qr-code__address-segments'; + private readonly addAccountConfirmButton = '[data-testid="submit-add-account-with-name"]'; private readonly addEthereumAccountButton = '[data-testid="multichain-account-menu-popover-add-account"]'; + private readonly addImportedAccountButton = + '[data-testid="multichain-account-menu-popover-add-imported-account"]'; + private readonly addSnapAccountButton = { text: 'Add account Snap', tag: 'button', @@ -33,24 +41,58 @@ class AccountListPage { private readonly createAccountButton = '[data-testid="multichain-account-menu-popover-action-button"]'; + private readonly currentSelectedAccount = + '.multichain-account-list-item--selected'; + private readonly editableLabelButton = '[data-testid="editable-label-button"]'; private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private readonly hideUnhideAccountButton = - '[data-testid="account-list-menu-hide"]'; - private readonly hiddenAccountOptionsMenuButton = '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; private readonly hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; + private readonly hideUnhideAccountButton = + '[data-testid="account-list-menu-hide"]'; + + private readonly importAccountConfirmButton = + '[data-testid="import-account-confirm-button"]'; + + private readonly importAccountPrivateKeyInput = '#private-key-box'; + + private readonly importAccountDropdownOption = '.dropdown__select'; + + private readonly importAccountJsonFileOption = { + text: 'JSON File', + tag: 'option', + }; + + private readonly importAccountJsonFileInput = + 'input[data-testid="file-input"]'; + + private readonly importAccountJsonPasswordInput = + 'input[id="json-password-box"]'; + private readonly pinUnpinAccountButton = '[data-testid="account-list-menu-pin"]'; private readonly pinnedIcon = '[data-testid="account-pinned-icon"]'; + private readonly removeAccountButton = + '[data-testid="account-list-menu-remove"]'; + + private readonly removeAccountConfirmButton = { + text: 'Remove', + tag: 'button', + }; + + private readonly removeAccountMessage = { + text: 'Remove account?', + tag: 'div', + }; + private readonly saveAccountLabelButton = '[data-testid="save-account-label-input"]'; @@ -81,11 +123,57 @@ class AccountListPage { await this.driver.clickElement(this.createAccountButton); await this.driver.clickElement(this.addEthereumAccountButton); await this.driver.fill(this.accountNameInput, customLabel); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await this.driver.delay(largeDelayMs); + await this.driver.clickElementAndWaitToDisappear( + this.addAccountConfirmButton, + ); + } + + /** + * Adds a new account with default next available name. + * + */ + async addNewAccountWithDefaultName(): Promise { + console.log(`Adding new account with next available name`); + await this.driver.clickElement(this.createAccountButton); + await this.driver.clickElement(this.addEthereumAccountButton); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await this.driver.delay(largeDelayMs); await this.driver.clickElementAndWaitToDisappear( this.addAccountConfirmButton, ); } + /** + * Import a new account with a private key. + * + * @param privateKey - Private key of the account + * @param expectedErrorMessage - Expected error message if the import should fail + */ + async addNewImportedAccount( + privateKey: string, + expectedErrorMessage?: string, + ): Promise { + console.log(`Adding new imported account`); + await this.driver.clickElement(this.createAccountButton); + await this.driver.clickElement(this.addImportedAccountButton); + await this.driver.fill(this.importAccountPrivateKeyInput, privateKey); + if (expectedErrorMessage) { + await this.driver.clickElement(this.importAccountConfirmButton); + await this.driver.waitForSelector({ + css: '.mm-help-text', + text: expectedErrorMessage, + }); + } else { + await this.driver.clickElementAndWaitToDisappear( + this.importAccountConfirmButton, + ); + } + } + /** * Changes the label of the current account. * @@ -112,6 +200,59 @@ class AccountListPage { await this.driver.clickElement(this.hideUnhideAccountButton); } + /** + * Import an account with a JSON file. + * + * @param jsonFilePath - Path to the JSON file to import + * @param password - Password for the imported account + */ + async importAccountWithJsonFile( + jsonFilePath: string, + password: string, + ): Promise { + console.log(`Adding new imported account`); + await this.driver.clickElement(this.createAccountButton); + await this.driver.clickElement(this.addImportedAccountButton); + await this.driver.clickElement(this.importAccountDropdownOption); + await this.driver.clickElement(this.importAccountJsonFileOption); + + const fileInput = await this.driver.findElement( + this.importAccountJsonFileInput, + ); + await fileInput.sendKeys(jsonFilePath); + await this.driver.fill(this.importAccountJsonPasswordInput, password); + await this.driver.clickElementAndWaitToDisappear( + this.importAccountConfirmButton, + ); + } + + /** + * Open the account details modal for the specified account in account list. + * + * @param accountLabel - The label of the account to open the details modal for. + */ + async openAccountDetailsModal(accountLabel: string): Promise { + console.log( + `Open account details modal in account list for account ${accountLabel}`, + ); + await this.openAccountOptionsInAccountList(accountLabel); + await this.driver.clickElement(this.accountMenuButton); + } + + /** + * Open the account options menu for the specified account. + * + * @param accountLabel - The label of the account to open the options menu for. + */ + async openAccountOptionsInAccountList(accountLabel: string): Promise { + console.log( + `Open account options in account list for account ${accountLabel}`, + ); + await this.driver.clickElement( + `button[data-testid="account-list-item-menu-button"][aria-label="${accountLabel} Options"]`, + ); + } + async openAccountOptionsMenu(): Promise { console.log(`Open account option menu`); await this.driver.waitForSelector(this.accountListItem); @@ -139,6 +280,19 @@ class AccountListPage { await this.driver.clickElement(this.pinUnpinAccountButton); } + /** + * Remove the specified account from the account list. + * + * @param accountLabel - The label of the account to remove. + */ + async removeAccount(accountLabel: string): Promise { + console.log(`Remove account in account list`); + await this.openAccountOptionsInAccountList(accountLabel); + await this.driver.clickElement(this.removeAccountButton); + await this.driver.waitForSelector(this.removeAccountMessage); + await this.driver.clickElement(this.removeAccountConfirmButton); + } + async switchToAccount(expectedLabel: string): Promise { console.log( `Switch to account with label ${expectedLabel} in account list`, @@ -223,10 +377,70 @@ class AccountListPage { await this.driver.assertElementNotPresent(this.addSnapAccountButton); } + /** + * Check that the correct address is displayed in the account details modal. + * + * @param expectedAddress - The expected address to check. + */ + async check_addressInAccountDetailsModal( + expectedAddress: string, + ): Promise { + console.log( + `Check that address ${expectedAddress} is displayed in account details modal`, + ); + await this.driver.waitForSelector(this.accountQrCodeImage); + await this.driver.waitForSelector({ + css: this.accountQrCodeAddress, + text: expectedAddress, + }); + } + + async check_currentAccountIsImported(): Promise { + console.log(`Check that current account is an imported account`); + await this.driver.waitForSelector({ + css: this.currentSelectedAccount, + text: 'Imported', + }); + } + async check_hiddenAccountsListExists(): Promise { console.log(`Check that hidden accounts list is displayed in account list`); await this.driver.waitForSelector(this.hiddenAccountsList); } + + /** + * Verifies number of accounts currently showing in the accounts menu. + * + * @param expectedNumberOfAccounts - The expected number of accounts showing. + */ + async check_numberOfAvailableAccounts( + expectedNumberOfAccounts: number, + ): Promise { + console.log( + `Verify the number of accounts in the account menu is: ${expectedNumberOfAccounts}`, + ); + await this.driver.wait(async () => { + const internalAccounts = await this.driver.findElements( + this.accountListItem, + ); + return internalAccounts.length === expectedNumberOfAccounts; + }, 20000); + } + + /** + * Check that the remove account button is not displayed in the account options menu for the specified account. + * + * @param accountLabel - The label of the account to check. + */ + async check_removeAccountButtonIsNotDisplayed( + accountLabel: string, + ): Promise { + console.log( + `Check that remove account button is not displayed in account options menu for account ${accountLabel} in account list`, + ); + await this.openAccountOptionsInAccountList(accountLabel); + await this.driver.assertElementNotPresent(this.removeAccountButton); + } } export default AccountListPage; diff --git a/test/e2e/page-objects/pages/developer-options-page.ts b/test/e2e/page-objects/pages/developer-options-page.ts new file mode 100644 index 000000000000..c15f6c767a82 --- /dev/null +++ b/test/e2e/page-objects/pages/developer-options-page.ts @@ -0,0 +1,38 @@ +import { Driver } from '../../webdriver/driver'; + +class DevelopOptions { + private readonly driver: Driver; + + // Locators + private readonly generatePageCrashButton: string = + '[data-testid="developer-options-generate-page-crash-button"]'; + + private readonly developOptionsPageTitle: object = { + text: 'Developer Options', + css: 'h4', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.developOptionsPageTitle); + } catch (e) { + console.log( + 'Timeout while waiting for Developer options page to be loaded', + e, + ); + throw e; + } + console.log('Developer option page is loaded'); + } + + async clickGenerateCrashButton(): Promise { + console.log('Generate a page crash in Developer option page'); + await this.driver.clickElement(this.generatePageCrashButton); + } +} + +export default DevelopOptions; diff --git a/test/e2e/page-objects/pages/dialog/edit-network.ts b/test/e2e/page-objects/pages/dialog/edit-network.ts new file mode 100644 index 000000000000..09a1dd70a5f9 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/edit-network.ts @@ -0,0 +1,54 @@ +import { Driver } from '../../../webdriver/driver'; + +class EditNetworkModal { + private driver: Driver; + + private readonly editModalNetworkNameInput = + '[data-testid="network-form-network-name"]'; + + private readonly editModalRpcDropDownButton = + '[data-testid="test-add-rpc-drop-down"]'; + + private readonly editModalSaveButton = { + text: 'Save', + tag: 'button', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.editModalNetworkNameInput, + this.editModalRpcDropDownButton, + this.editModalSaveButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for select network dialog to be loaded', + e, + ); + throw e; + } + console.log('Edit network dialog is loaded'); + } + + /** + * Selects an RPC from the dropdown in the edit network modal. + * + * @param rpcName - The name of the RPC to select. + */ + async selectRPCInEditNetworkModal(rpcName: string): Promise { + console.log(`Select RPC ${rpcName} in edit network modal`); + await this.driver.clickElement(this.editModalRpcDropDownButton); + await this.driver.clickElement({ + text: rpcName, + tag: 'button', + }); + await this.driver.clickElementAndWaitToDisappear(this.editModalSaveButton); + } +} + +export default EditNetworkModal; diff --git a/test/e2e/page-objects/pages/dialog/network-switch-modal-confirmation.ts b/test/e2e/page-objects/pages/dialog/network-switch-modal-confirmation.ts new file mode 100644 index 000000000000..8a51194605f7 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/network-switch-modal-confirmation.ts @@ -0,0 +1,39 @@ +import { Driver } from '../../../webdriver/driver'; + +class NetworkSwitchModalConfirmation { + private driver: Driver; + + private readonly submitButton = '[data-testid="confirmation-submit-button"]'; + + private readonly addNetworkMessage = { + text: 'Want to add this network?', + tag: 'h3', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.addNetworkMessage, + this.submitButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for add network confirmation modal to be loaded', + e, + ); + throw e; + } + console.log('Add network confirmation modal is loaded'); + } + + async clickApproveButton(): Promise { + console.log('Click Approve Button'); + await this.driver.clickElementAndWaitToDisappear(this.submitButton); + } +} + +export default NetworkSwitchModalConfirmation; diff --git a/test/e2e/page-objects/pages/dialog/select-network.ts b/test/e2e/page-objects/pages/dialog/select-network.ts new file mode 100644 index 000000000000..bc20c42855ae --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/select-network.ts @@ -0,0 +1,125 @@ +import { Driver } from '../../../webdriver/driver'; + +class SelectNetwork { + private driver: Driver; + + private readonly addNetworkButton = '[data-testid="test-add-button"]'; + + private readonly closeButton = 'button[aria-label="Close"]'; + + private readonly editNetworkButton = + '[data-testid="network-list-item-options-edit"]'; + + private readonly rpcUrlItem = '.select-rpc-url__item'; + + private readonly searchInput = + '[data-testid="network-redesign-modal-search-input"]'; + + private readonly selectNetworkMessage = { + text: 'Select a network', + tag: 'h4', + }; + + private readonly selectRpcMessage = { + text: 'Select RPC URL', + tag: 'h4', + }; + + private readonly toggleButton = '.toggle-button > div'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.selectNetworkMessage, + this.searchInput, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for select network dialog to be loaded', + e, + ); + throw e; + } + console.log('Select network dialog is loaded'); + } + + async clickAddButton(): Promise { + console.log('Click Add Button'); + await this.driver.clickElementAndWaitToDisappear(this.addNetworkButton); + } + + async clickCloseButton(): Promise { + console.log('Click Close Button'); + await this.driver.clickElementAndWaitToDisappear(this.closeButton); + } + + async fillNetworkSearchInput(networkName: string): Promise { + console.log(`Fill network search input with ${networkName}`); + await this.driver.fill(this.searchInput, networkName); + } + + async openEditNetworkModal(): Promise { + console.log('Open edit network modal'); + await this.driver.clickElementAndWaitToDisappear(this.editNetworkButton); + } + + async openNetworkListOptions(chainId: string): Promise { + console.log(`Open network options for ${chainId} in network dialog`); + await this.driver.clickElement( + `[data-testid="network-list-item-options-button-${chainId}"]`, + ); + } + + async openNetworkRPC(chainId: string): Promise { + console.log(`Open network RPC ${chainId}`); + await this.driver.clickElementAndWaitToDisappear( + `[data-testid="network-rpc-name-button-${chainId}"]`, + ); + await this.driver.waitForSelector(this.selectRpcMessage); + } + + async selectNetworkName(networkName: string): Promise { + console.log(`Click ${networkName}`); + const networkNameItem = `[data-testid="${networkName}"]`; + await this.driver.clickElementAndWaitToDisappear(networkNameItem); + } + + async selectRPC(rpcName: string): Promise { + console.log(`Select RPC ${rpcName} for network`); + await this.driver.waitForSelector(this.selectRpcMessage); + await this.driver.clickElementAndWaitToDisappear({ + text: rpcName, + tag: 'button', + }); + } + + async toggleShowTestNetwork(): Promise { + console.log('Toggle show test network in select network dialog'); + await this.driver.clickElement(this.toggleButton); + } + + async check_networkRPCNumber(expectedNumber: number): Promise { + console.log( + `Wait for ${expectedNumber} RPC URLs to be displayed in select network dialog`, + ); + await this.driver.wait(async () => { + const rpcNumber = await this.driver.findElements(this.rpcUrlItem); + return rpcNumber.length === expectedNumber; + }, 10000); + console.log(`${expectedNumber} RPC URLs found in select network dialog`); + } + + async check_rpcIsSelected(rpcName: string): Promise { + console.log(`Check RPC ${rpcName} is selected in network dialog`); + await this.driver.waitForSelector({ + text: rpcName, + tag: 'button', + }); + } +} + +export default SelectNetwork; diff --git a/test/e2e/page-objects/pages/error-page.ts b/test/e2e/page-objects/pages/error-page.ts new file mode 100644 index 000000000000..76acacdcf9dd --- /dev/null +++ b/test/e2e/page-objects/pages/error-page.ts @@ -0,0 +1,95 @@ +import { Driver } from '../../webdriver/driver'; +import HeaderNavbar from './header-navbar'; +import SettingsPage from './settings-page'; +import DevelopOptionsPage from './developer-options-page'; + +const FEEDBACK_MESSAGE = + 'Message: Unable to find value of key "developerOptions" for locale "en"'; + +class ErrorPage { + private readonly driver: Driver; + + // Locators + private readonly errorPageTitle: object = { + text: 'MetaMask encountered an error', + css: 'h3', + }; + + private readonly errorMessage = '[data-testid="error-page-error-message"]'; + + private readonly sendReportToSentryButton = + '[data-testid="error-page-describe-what-happened-button"]'; + + private readonly sentryReportForm = + '[data-testid="error-page-sentry-feedback-modal"]'; + + private readonly contactSupportButton = + '[data-testid="error-page-contact-support-button"]'; + + private readonly sentryFeedbackTextarea = + '[data-testid="error-page-sentry-feedback-textarea"]'; + + private readonly sentryFeedbackSubmitButton = + '[data-testid="error-page-sentry-feedback-submit-button"]'; + + private readonly sentryFeedbackSuccessModal = + '[data-testid="error-page-sentry-feedback-success-modal"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.errorPageTitle); + } catch (e) { + console.log('Timeout while waiting for Error page to be loaded', e); + throw e; + } + console.log('Error page is loaded'); + } + + async triggerPageCrash(): Promise { + const headerNavbar = new HeaderNavbar(this.driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(this.driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptionsPage(this.driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); + } + + async validate_errorMessage(): Promise { + await this.driver.waitForSelector({ + text: `Message: Unable to find value of key "developerOptions" for locale "en"`, + css: this.errorMessage, + }); + } + + async submitToSentryUserFeedbackForm(): Promise { + console.log(`Open sentry user feedback form in error page`); + await this.driver.clickElement(this.sendReportToSentryButton); + await this.driver.waitForSelector(this.sentryReportForm); + await this.driver.fill(this.sentryFeedbackTextarea, FEEDBACK_MESSAGE); + await this.driver.clickElementAndWaitToDisappear( + this.sentryFeedbackSubmitButton, + ); + } + + async contactAndValidateMetaMaskSupport(): Promise { + console.log(`Contact metamask support form in a separate page`); + await this.driver.waitUntilXWindowHandles(1); + await this.driver.clickElement(this.contactSupportButton); + // metamask, help page + await this.driver.waitUntilXWindowHandles(2); + } + + async waitForSentrySuccessModal(): Promise { + await this.driver.waitForSelector(this.sentryFeedbackSuccessModal); + await this.driver.assertElementNotPresent(this.sentryFeedbackSuccessModal); + } +} + +export default ErrorPage; diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 8bd29ea8c602..100c23b851e4 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -5,7 +5,7 @@ class HeaderNavbar { private readonly accountMenuButton = '[data-testid="account-menu-icon"]'; - private readonly accountOptionMenu = + private readonly threeDotMenuButton = '[data-testid="account-options-menu-button"]'; private readonly accountSnapButton = { text: 'Snaps', tag: 'div' }; @@ -17,6 +17,8 @@ class HeaderNavbar { private readonly settingsButton = '[data-testid="global-menu-settings"]'; + private readonly switchNetworkDropDown = '[data-testid="network-display"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -25,7 +27,7 @@ class HeaderNavbar { try { await this.driver.waitForMultipleSelectors([ this.accountMenuButton, - this.accountOptionMenu, + this.threeDotMenuButton, ]); } catch (e) { console.log('Timeout while waiting for header navbar to be loaded', e); @@ -35,11 +37,7 @@ class HeaderNavbar { } 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.openThreeDotMenu(); await this.driver.clickElement(this.lockMetaMaskButton); } @@ -47,22 +45,39 @@ class HeaderNavbar { await this.driver.clickElement(this.accountMenuButton); } + async openThreeDotMenu(): Promise { + console.log('Open account options menu'); + await this.driver.clickElement(this.threeDotMenuButton); + // fix race condition with mmi build + if (process.env.MMI) { + await this.driver.waitForSelector(this.mmiPortfolioButton); + } + } + async openSnapListPage(): Promise { console.log('Open account snap page'); - await this.driver.clickElement(this.accountOptionMenu); + await this.openThreeDotMenu(); 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.openThreeDotMenu(); await this.driver.clickElement(this.settingsButton); } + async clickSwitchNetworkDropDown(): Promise { + console.log(`Click switch network menu`); + await this.driver.clickElement(this.switchNetworkDropDown); + } + + async check_currentSelectedNetwork(networkName: string): Promise { + console.log(`Validate the Switch network to ${networkName}`); + await this.driver.waitForSelector( + `button[data-testid="network-display"][aria-label="Network Menu ${networkName}"]`, + ); + } + /** * Verifies that the displayed account label in header matches the expected label. * diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 326ecc3188b7..101e6f9de83c 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -11,8 +11,22 @@ class HomePage { private readonly activityTab = '[data-testid="account-overview__activity-tab"]'; + private readonly nftTab = '[data-testid="account-overview__nfts-tab"]'; + + private readonly nftIconOnActivityList = '[data-testid="nft-item"]'; + private readonly balance = '[data-testid="eth-overview__primary-currency"]'; + private readonly basicFunctionalityOffWarningMessage = { + text: 'Basic functionality is off', + css: '.mm-banner-alert', + }; + + private readonly closeUseNetworkNotificationModalButton = { + text: 'Got it', + tag: 'h6', + }; + private readonly completedTransactions = '[data-testid="activity-list-item"]'; private readonly confirmedTransactions = { @@ -25,6 +39,8 @@ class HomePage { css: '.transaction-status-label--failed', }; + private readonly popoverBackground = '.popover-bg'; + private readonly sendButton = '[data-testid="eth-overview-send"]'; private readonly tokensTab = '[data-testid="account-overview__asset-tab"]'; @@ -51,8 +67,16 @@ class HomePage { console.log('Home page is loaded'); } - async startSendFlow(): Promise { - await this.driver.clickElement(this.sendButton); + async closeUseNetworkNotificationModal(): Promise { + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#25788) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await this.driver.assertElementNotPresent(this.popoverBackground); + await this.driver.clickElementSafe( + this.closeUseNetworkNotificationModalButton, + ); + await this.driver.assertElementNotPresent( + this.closeUseNetworkNotificationModalButton, + ); } async goToActivityList(): Promise { @@ -60,28 +84,39 @@ class HomePage { await this.driver.clickElement(this.activityTab); } + async goToNFTList(): Promise { + console.log(`Open NFT tab on homepage`); + await this.driver.clickElement(this.nftTab); + } + + async clickNFTIconOnActivityList() { + await this.driver.clickElement(this.nftIconOnActivityList); + } + + async startSendFlow(): Promise { + await this.driver.clickElement(this.sendButton); + } + /** - * This function checks if the specified number of confirmed transactions are displayed in the activity list on homepage. - * It waits up to 10 seconds for the expected number of confirmed transactions to be visible. + * Checks if the toaster message for adding a network is displayed on the homepage. * - * @param expectedNumber - The number of confirmed transactions expected to be displayed in activity list. Defaults to 1. - * @returns A promise that resolves if the expected number of confirmed transactions is displayed within the timeout period. + * @param networkName - The name of the network that was added. */ - async check_confirmedTxNumberDisplayedInActivity( - expectedNumber: number = 1, - ): Promise { + async check_addNetworkMessageIsDisplayed(networkName: string): Promise { console.log( - `Wait for ${expectedNumber} confirmed transactions to be displayed in activity list`, + `Check the toaster message for adding network ${networkName} is displayed on homepage`, ); - await this.driver.wait(async () => { - const confirmedTxs = await this.driver.findElements( - this.confirmedTransactions, - ); - return confirmedTxs.length === expectedNumber; - }, 10000); + await this.driver.waitForSelector({ + tag: 'h6', + text: `“${networkName}” was successfully added!`, + }); + } + + async check_basicFunctionalityOffWarnigMessageIsDisplayed(): Promise { console.log( - `${expectedNumber} confirmed transactions found in activity list on homepage`, + 'Check if basic functionality off warning message is displayed on homepage', ); + await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); } /** @@ -108,6 +143,47 @@ class HomePage { ); } + /** + * This function checks if the specified number of confirmed transactions are displayed in the activity list on homepage. + * It waits up to 10 seconds for the expected number of confirmed transactions to be visible. + * + * @param expectedNumber - The number of confirmed transactions expected to be displayed in activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of confirmed transactions is displayed within the timeout period. + */ + async check_confirmedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise { + console.log( + `Wait for ${expectedNumber} confirmed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const confirmedTxs = await this.driver.findElements( + this.confirmedTransactions, + ); + return confirmedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} confirmed transactions found in activity list on homepage`, + ); + } + + /** + * Checks if the toaster message for editing a network is displayed on the homepage. + * + * @param networkName - The name of the network that was edited. + */ + async check_editNetworkMessageIsDisplayed( + networkName: string, + ): Promise { + console.log( + `Check the toaster message for editing network ${networkName} is displayed on homepage`, + ); + await this.driver.waitForSelector({ + tag: 'h6', + text: `“${networkName}” was successfully edited!`, + }); + } + /** * Checks if the expected balance is displayed on homepage. * diff --git a/test/e2e/page-objects/pages/nft-details-page.ts b/test/e2e/page-objects/pages/nft-details-page.ts new file mode 100644 index 000000000000..b95477f9024d --- /dev/null +++ b/test/e2e/page-objects/pages/nft-details-page.ts @@ -0,0 +1,17 @@ +import { Driver } from '../../webdriver/driver'; + +class NFTDetailsPage { + private driver: Driver; + + private readonly nftSendButton = '[data-testid="nft-send-button"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async clickNFTSendButton() { + await this.driver.clickElement(this.nftSendButton); + } +} + +export default NFTDetailsPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-complete-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-complete-page.ts new file mode 100644 index 000000000000..827f89899bad --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-complete-page.ts @@ -0,0 +1,95 @@ +import { Driver } from '../../../webdriver/driver'; + +class OnboardingCompletePage { + private driver: Driver; + + private readonly congratulationsMessage = { + text: 'Congratulations!', + tag: 'h2', + }; + + private readonly defaultPrivacySettingsButton = { + text: 'Manage default privacy settings', + tag: 'button', + }; + + private readonly installCompleteMessage = { + text: 'Your MetaMask install is complete!', + tag: 'h2', + }; + + private readonly onboardingCompleteDoneButton = + '[data-testid="onboarding-complete-done"]'; + + private readonly pinExtensionDoneButton = + '[data-testid="pin-extension-done"]'; + + private readonly pinExtensionMessage = { + text: 'Click browser extension icon to access it instantly', + tag: 'p', + }; + + private readonly pinExtensionNextButton = + '[data-testid="pin-extension-next"]'; + + private readonly walletReadyMessage = { + text: 'Your wallet is ready', + tag: 'h2', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.defaultPrivacySettingsButton, + this.onboardingCompleteDoneButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding wallet creation complete page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding wallet creation complete page is loaded'); + } + + async clickCreateWalletDoneButton(): Promise { + await this.driver.clickElementAndWaitToDisappear( + this.onboardingCompleteDoneButton, + ); + } + + async completeOnboarding(): Promise { + console.log('Complete onboarding'); + await this.clickCreateWalletDoneButton(); + await this.driver.waitForSelector(this.installCompleteMessage); + await this.driver.clickElement(this.pinExtensionNextButton); + + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.waitForSelector(this.pinExtensionMessage); + await this.driver.waitForElementToStopMoving(this.pinExtensionDoneButton); + await this.driver.clickElementAndWaitToDisappear( + this.pinExtensionDoneButton, + ); + } + + async navigateToDefaultPrivacySettings(): Promise { + await this.driver.clickElementAndWaitToDisappear( + this.defaultPrivacySettingsButton, + ); + } + + async check_congratulationsMessageIsDisplayed(): Promise { + await this.driver.waitForSelector(this.congratulationsMessage); + } + + async check_walletReadyMessageIsDisplayed(): Promise { + await this.driver.waitForSelector(this.walletReadyMessage); + } +} + +export default OnboardingCompletePage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-metrics-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-metrics-page.ts new file mode 100644 index 000000000000..2982acaa40c0 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-metrics-page.ts @@ -0,0 +1,38 @@ +import { Driver } from '../../../webdriver/driver'; + +class OnboardingMetricsPage { + private driver: Driver; + + private readonly metametricsMessage = { + text: 'Help us improve MetaMask', + tag: 'h2', + }; + + private readonly noThanksButton = '[data-testid="metametrics-no-thanks"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.metametricsMessage, + this.noThanksButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding metametrics page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding metametrics page is loaded'); + } + + async clickNoThanksButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.noThanksButton); + } +} + +export default OnboardingMetricsPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts new file mode 100644 index 000000000000..ac8dd4f923f2 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts @@ -0,0 +1,108 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../../webdriver/driver'; +import { WALLET_PASSWORD } from '../../../helpers'; + +class OnboardingPasswordPage { + private driver: Driver; + + private readonly confirmPasswordInput = + '[data-testid="create-password-confirm"]'; + + private readonly createPasswordMessage = { + text: 'Create password', + tag: 'h2', + }; + + private readonly createWalletButton = + '[data-testid="create-password-wallet"]'; + + private readonly importWalletButton = + '[data-testid="create-password-import"]'; + + private readonly incorrectPasswordWarningMessage = { + text: "Passwords don't match", + tag: 'h6', + }; + + private readonly newPasswordInput = '[data-testid="create-password-new"]'; + + private readonly passwordTerms = '[data-testid="create-password-terms"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.createPasswordMessage, + this.newPasswordInput, + this.confirmPasswordInput, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for create password page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding password page is loaded'); + } + + /** + * Create a password for new imported wallet + * + * @param password - The password to create. Defaults to WALLET_PASSWORD. + */ + async createImportedWalletPassword( + password: string = WALLET_PASSWORD, + ): Promise { + console.log('Create password for new imported wallet'); + await this.fillWalletPassword(password, password); + await this.driver.clickElementAndWaitToDisappear(this.importWalletButton); + } + + /** + * Create a password for new created wallet + * + * @param password - The new password to create. Defaults to WALLET_PASSWORD. + */ + async createWalletPassword( + password: string = WALLET_PASSWORD, + ): Promise { + console.log('Create password for new created wallet'); + await this.fillWalletPassword(password, password); + await this.driver.clickElementAndWaitToDisappear(this.createWalletButton); + } + + /** + * Fill the wallet password fields + * + * @param newPassword - The new password to fill. + * @param confirmPassword - The confirm password to fill. + */ + async fillWalletPassword( + newPassword: string, + confirmPassword: string, + ): Promise { + console.log('Fill the wallet password fields'); + await this.driver.fill(this.newPasswordInput, newPassword); + await this.driver.fill(this.confirmPasswordInput, confirmPassword); + await this.driver.clickElement(this.passwordTerms); + } + + async check_confirmPasswordButtonIsDisabled(): Promise { + console.log('Check the confirm password button is disabled'); + const confirmPasswordButton = await this.driver.findElement( + this.createWalletButton, + ); + assert.equal(await confirmPasswordButton.isEnabled(), false); + } + + async check_incorrectPasswordWarningMessageIsDisplayed(): Promise { + console.log('Check the incorrect password warning message is displayed'); + await this.driver.waitForSelector(this.incorrectPasswordWarningMessage); + } +} + +export default OnboardingPasswordPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts new file mode 100644 index 000000000000..e8288edb98cb --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts @@ -0,0 +1,212 @@ +import { Driver } from '../../../webdriver/driver'; + +class OnboardingPrivacySettingsPage { + private driver: Driver; + + private readonly assetsSettings = '[data-testid="category-item-Assets"]'; + + private readonly categoryBackButton = '[data-testid="category-back-button"]'; + + private readonly generalSettings = '[data-testid="category-item-General"]'; + + private readonly privacySettingsBackButton = + '[data-testid="privacy-settings-back-button"]'; + + private readonly securitySettings = '[data-testid="category-item-Security"]'; + + // General settings + private readonly basicFunctionalityCheckbox = + '[id="basic-configuration-checkbox"]'; + + private readonly basicFunctionalityToggle = + '[data-testid="basic-functionality-toggle"] .toggle-button'; + + private readonly basicFunctionalityTurnOffButton = { + text: 'Turn off', + tag: 'button', + }; + + private readonly basicFunctionalityTurnOffMessage = { + text: 'Turn off basic functionality', + tag: 'h4', + }; + + private readonly generalSettingsMessage = { text: 'General', tag: 'h2' }; + + // General settings - add custom network section + private readonly addCustomNetworkButton = { + text: 'Add a network', + tag: 'p', + }; + + private readonly addCustomNetworkFormMessage = { + text: 'Add a custom network', + tag: 'h4', + }; + + private readonly addRpcUrlButton = { + text: 'Add RPC URL', + tag: 'button', + }; + + private readonly addRpcUrlDialogMessage = { + text: 'Add RPC URL', + tag: 'h4', + }; + + private readonly addRpcUrlDropDown = '[data-testid="test-add-rpc-drop-down"]'; + + private readonly chainIdInput = '[data-testid="network-form-chain-id"]'; + + private readonly confirmAddCustomNetworkButton = { + text: 'Save', + tag: 'button', + }; + + private readonly confirmAddRpcUrlButton = { + text: 'Add URL', + tag: 'button', + }; + + private readonly currencySymbolInput = + '[data-testid="network-form-ticker-input"]'; + + private readonly networkNameInput = + '[data-testid="network-form-network-name"]'; + + private readonly rpcUrlInput = '[data-testid="rpc-url-input-test"]'; + + // Assets settings + private readonly assetsPrivacyToggle = '.toggle-button.toggle-button--on'; + + private readonly assetsSettingsMessage = { text: 'Assets', tag: 'h2' }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.generalSettings, + this.assetsSettings, + this.securitySettings, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding privacy settings page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding privacy settings page is loaded'); + } + + /** + * Adds a custom network to MetaMask during the onboarding process. + * + * @param networkName - The name of the custom network. + * @param chainId - The chain ID of the custom network. + * @param currencySymbol - The currency symbol for the custom network. + * @param networkUrl - The RPC URL for the custom network. + * @returns A promise that resolves when the custom network has been added. + */ + async addCustomNetwork( + networkName: string, + chainId: number, + currencySymbol: string, + networkUrl: string, + ): Promise { + await this.navigateToGeneralSettings(); + console.log('Adding custom network'); + await this.driver.clickElement(this.addCustomNetworkButton); + await this.driver.waitForSelector(this.addCustomNetworkFormMessage); + await this.driver.fill(this.networkNameInput, networkName); + await this.driver.fill(this.chainIdInput, chainId.toString()); + await this.driver.fill(this.currencySymbolInput, currencySymbol); + // Add rpc url + await this.driver.clickElement(this.addRpcUrlDropDown); + await this.driver.clickElement(this.addRpcUrlButton); + await this.driver.waitForSelector(this.addRpcUrlDialogMessage); + await this.driver.fill(this.rpcUrlInput, networkUrl); + await this.driver.clickElement(this.confirmAddRpcUrlButton); + await this.driver.clickElementAndWaitToDisappear( + this.confirmAddCustomNetworkButton, + ); + // Navigate back to default privacy settings + await this.navigateBackToSettingsPage(); + } + + /** + * Navigate back to the onboarding complete page. + */ + async navigateBackToOnboardingCompletePage(): Promise { + console.log('Navigate back to onboarding complete page'); + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.waitForElementToStopMoving( + this.privacySettingsBackButton, + ); + await this.driver.clickElementAndWaitToDisappear( + this.privacySettingsBackButton, + ); + } + + /** + * Navigate back to the onboarding privacy settings page. + */ + async navigateBackToSettingsPage(): Promise { + console.log('Navigate back to onboarding privacy settings page'); + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + + async navigateToGeneralSettings(): Promise { + console.log('Navigate to general settings'); + await this.check_pageIsLoaded(); + await this.driver.clickElement(this.generalSettings); + await this.driver.waitForSelector(this.generalSettingsMessage); + } + + /** + * Open the edit network modal for a given network name. + * + * @param networkName - The name of the network to open the edit modal for. + */ + async openEditNetworkModal(networkName: string): Promise { + console.log(`Open edit network modal for ${networkName}`); + await this.driver.clickElement({ text: networkName, tag: 'p' }); + await this.driver.waitForSelector(this.addRpcUrlDropDown); + } + + /** + * Go to assets settings and toggle options, then navigate back. + */ + async toggleAssetsSettings(): Promise { + console.log('Toggle advanced assets settings in privacy settings'); + await this.check_pageIsLoaded(); + await this.driver.clickElement(this.assetsSettings); + await this.driver.waitForSelector(this.assetsSettingsMessage); + await Promise.all( + ( + await this.driver.findClickableElements(this.assetsPrivacyToggle) + ).map((toggle) => toggle.click()), + ); + await this.navigateBackToSettingsPage(); + } + + /** + * Go to general settings and toggle options, then navigate back. + */ + async toggleBasicFunctionalitySettings(): Promise { + console.log('Toggle basic functionality settings in privacy settings'); + await this.navigateToGeneralSettings(); + await this.driver.clickElement(this.basicFunctionalityToggle); + await this.driver.waitForSelector(this.basicFunctionalityTurnOffMessage); + await this.driver.clickElement(this.basicFunctionalityCheckbox); + await this.driver.clickElement(this.basicFunctionalityTurnOffButton); + await this.navigateBackToSettingsPage(); + } +} + +export default OnboardingPrivacySettingsPage; diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts new file mode 100644 index 000000000000..37d1d640670f --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts @@ -0,0 +1,120 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../../webdriver/driver'; +import { E2E_SRP } from '../../../default-fixture'; + +class OnboardingSrpPage { + private driver: Driver; + + private readonly srpConfirmButton = '[data-testid="import-srp-confirm"]'; + + private readonly srpDropdown = '.import-srp__number-of-words-dropdown'; + + private readonly srpDropdownOptions = + '.import-srp__number-of-words-dropdown option'; + + private readonly srpMessage = { + text: 'Access your wallet with your Secret Recovery Phrase', + tag: 'h2', + }; + + private readonly srpWord0 = '[data-testid="import-srp__srp-word-0"]'; + + private readonly srpWords = '.import-srp__srp-word'; + + private readonly wrongSrpWarningMessage = { + text: 'Invalid Secret Recovery Phrase', + css: '.import-srp__banner-alert-text', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.srpMessage, + this.srpWord0, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for onboarding srp page to be loaded', + e, + ); + throw e; + } + console.log('Onboarding srp page is loaded'); + } + + async clickConfirmButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.srpConfirmButton); + } + + /** + * Fill the SRP words with the provided seed phrase + * + * @param seedPhrase - The seed phrase to fill. Defaults to E2E_SRP. + */ + async fillSrp(seedPhrase: string = E2E_SRP): Promise { + await this.driver.pasteIntoField(this.srpWord0, seedPhrase); + } + + /** + * Fill the SRP words with the provided seed phrase word by word + * + * @param seedPhrase - The seed phrase to fill. Defaults to E2E_SRP. + */ + async fillSrpWordByWord(seedPhrase: string = E2E_SRP): Promise { + const words = seedPhrase.split(' '); + for (const word of words) { + await this.driver.pasteIntoField( + `[data-testid="import-srp__srp-word-${words.indexOf(word)}"]`, + word, + ); + } + } + + async check_confirmSrpButtonIsDisabled(): Promise { + console.log('Check that confirm SRP button is disabled'); + const confirmSeedPhrase = await this.driver.findElement( + this.srpConfirmButton, + ); + assert.equal(await confirmSeedPhrase.isEnabled(), false); + } + + /** + * Check the SRP dropdown iterates through each option + * + * @param numOptions - The number of options to check. Defaults to 5. + */ + async check_srpDropdownIterations(numOptions: number = 5) { + console.log( + `Check the SRP dropdown iterates through ${numOptions} options`, + ); + await this.driver.clickElement(this.srpDropdown); + await this.driver.wait(async () => { + const options = await this.driver.findElements(this.srpDropdownOptions); + return options.length === numOptions; + }, this.driver.timeout); + + const options = await this.driver.findElements(this.srpDropdownOptions); + for (let i = 0; i < options.length; i++) { + if (i !== 0) { + await this.driver.clickElement(this.srpDropdown); + } + await options[i].click(); + const expectedNumFields = 12 + i * 3; + await this.driver.wait(async () => { + const srpWordsFields = await this.driver.findElements(this.srpWords); + return expectedNumFields === srpWordsFields.length; + }, this.driver.timeout); + } + } + + async check_wrongSrpWarningMessage(): Promise { + console.log('Check that wrong SRP warning message is displayed'); + await this.driver.waitForSelector(this.wrongSrpWarningMessage); + } +} + +export default OnboardingSrpPage; diff --git a/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts b/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts new file mode 100644 index 000000000000..cff7549a0f75 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/secure-wallet-page.ts @@ -0,0 +1,107 @@ +import { Driver } from '../../../webdriver/driver'; + +class SecureWalletPage { + private driver: Driver; + + private readonly confirmRecoveryPhraseButton = + '[data-testid="recovery-phrase-confirm"]'; + + private readonly confirmSecretRecoveryPhraseMessage = { + text: 'Confirm Secret Recovery Phrase', + tag: 'h2', + }; + + private readonly recoveryPhraseChips = + '[data-testid="recovery-phrase-chips"]'; + + private readonly recoveryPhraseInputIndex2 = + '[data-testid="recovery-phrase-input-2"]'; + + private readonly recoveryPhraseInputIndex3 = + '[data-testid="recovery-phrase-input-3"]'; + + private readonly recoveryPhraseInputIndex7 = + '[data-testid="recovery-phrase-input-7"]'; + + private readonly recoveryPhraseNextButton = + '[data-testid="recovery-phrase-next"]'; + + private readonly revealSecretRecoveryPhraseButton = + '[data-testid="recovery-phrase-reveal"]'; + + private readonly secureWalletButton = + '[data-testid="secure-wallet-recommended"]'; + + private readonly secureWalletLaterButton = + '[data-testid="secure-wallet-later"]'; + + private readonly secureWalletMessage = { + text: 'Secure your wallet', + tag: 'h2', + }; + + private readonly writeDownSecretRecoveryPhraseMessage = { + text: 'Write down your Secret Recovery Phrase', + tag: 'h2', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.secureWalletMessage, + this.secureWalletButton, + this.secureWalletLaterButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for secure wallet page to be loaded', + e, + ); + throw e; + } + console.log('Secure wallet page is loaded'); + } + + async revealAndConfirmSRP(): Promise { + console.log( + 'Reveal and confirm SRP on secure wallet page during onboarding', + ); + // click secure my wallet button to reveal SRP + await this.driver.clickElement(this.secureWalletButton); + await this.driver.waitForMultipleSelectors([ + this.writeDownSecretRecoveryPhraseMessage, + this.revealSecretRecoveryPhraseButton, + ]); + + // click reveal button to reveal SRP + await this.driver.clickElement(this.revealSecretRecoveryPhraseButton); + await this.driver.waitForSelector(this.recoveryPhraseChips); + + let finalWords: string[] = []; + await this.driver.wait(async () => { + const recoveryPhraseChips = await this.driver.findElement( + this.recoveryPhraseChips, + ); + const recoveryPhrase = await recoveryPhraseChips.getText(); + const words = recoveryPhrase.split(/\s*(?:[0-9)]+|\n|\.|^$|$)\s*/u); + finalWords = words.filter((str) => str !== ''); + return finalWords.length === 12; + }, this.driver.timeout); + await this.driver.clickElement(this.recoveryPhraseNextButton); + + // confirm SRP + await this.driver.waitForSelector(this.confirmSecretRecoveryPhraseMessage); + await this.driver.fill(this.recoveryPhraseInputIndex2, finalWords[2]); + await this.driver.fill(this.recoveryPhraseInputIndex3, finalWords[3]); + await this.driver.fill(this.recoveryPhraseInputIndex7, finalWords[7]); + await this.driver.clickElementAndWaitToDisappear( + this.confirmRecoveryPhraseButton, + ); + } +} + +export default SecureWalletPage; diff --git a/test/e2e/page-objects/pages/onboarding/start-onboarding-page.ts b/test/e2e/page-objects/pages/onboarding/start-onboarding-page.ts new file mode 100644 index 000000000000..47c0b53a2b2f --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/start-onboarding-page.ts @@ -0,0 +1,52 @@ +import { Driver } from '../../../webdriver/driver'; + +class StartOnboardingPage { + private driver: Driver; + + private readonly createWalletButton = + '[data-testid="onboarding-create-wallet"]'; + + private readonly importWalletButton = + '[data-testid="onboarding-import-wallet"]'; + + private readonly startMessage = { + text: "Let's get started", + tag: 'h2', + }; + + private readonly termsCheckbox = '[data-testid="onboarding-terms-checkbox"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.startMessage, + this.termsCheckbox, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for start onboarding page to be loaded', + e, + ); + throw e; + } + console.log('Start onboarding page is loaded'); + } + + async checkTermsCheckbox(): Promise { + await this.driver.clickElement(this.termsCheckbox); + } + + async clickCreateWalletButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.createWalletButton); + } + + async clickImportWalletButton(): Promise { + await this.driver.clickElementAndWaitToDisappear(this.importWalletButton); + } +} + +export default StartOnboardingPage; 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 728afbfdd4df..29222ed3d48c 100644 --- a/test/e2e/page-objects/pages/send/send-token-page.ts +++ b/test/e2e/page-objects/pages/send/send-token-page.ts @@ -1,44 +1,37 @@ import { strict as assert } from 'assert'; import { Driver } from '../../../webdriver/driver'; -import { RawLocator } from '../../common'; class SendTokenPage { private driver: Driver; - private inputRecipient: string; + private readonly assetPickerButton = '[data-testid="asset-picker-button"]'; - private inputAmount: string; + private readonly continueButton = { + text: 'Continue', + tag: 'button', + }; - private scanButton: string; + private readonly ensAddressAsRecipient = '[data-testid="ens-input-selected"]'; - private continueButton: object; + private readonly ensResolvedName = + '[data-testid="multichain-send-page__recipient__item__title"]'; - private ensResolvedName: string; + private readonly inputAmount = '[data-testid="currency-input"]'; - private ensAddressAsRecipient: string; + private readonly inputNFTAmount = '[data-testid="nft-input"]'; - private ensResolvedAddress: string; + private readonly inputRecipient = '[data-testid="ens-input"]'; - private assetPickerButton: RawLocator; + private readonly recipientAccount = + '.multichain-account-list-item__account-name__button'; - private tokenListButton: RawLocator; + private readonly scanButton = '[data-testid="ens-qr-scan-button"]'; + + private readonly tokenListButton = + '[data-testid="multichain-token-list-button"]'; constructor(driver: Driver) { this.driver = driver; - this.inputAmount = '[data-testid="currency-input"]'; - this.inputRecipient = '[data-testid="ens-input"]'; - this.scanButton = '[data-testid="ens-qr-scan-button"]'; - this.ensResolvedName = - '[data-testid="multichain-send-page__recipient__item__title"]'; - this.ensResolvedAddress = - '[data-testid="multichain-send-page__recipient__item__subtitle"]'; - this.ensAddressAsRecipient = '[data-testid="ens-input-selected"]'; - this.continueButton = { - text: 'Continue', - tag: 'button', - }; - this.assetPickerButton = '[data-testid="asset-picker-button"]'; - this.tokenListButton = '[data-testid="multichain-token-list-button"]'; } async check_pageIsLoaded(): Promise { @@ -57,11 +50,13 @@ class SendTokenPage { console.log('Send token screen is loaded'); } - async fillRecipient(recipientAddress: string): Promise { - console.log( - `Fill recipient input with ${recipientAddress} on send token screen`, - ); - await this.driver.pasteIntoField(this.inputRecipient, recipientAddress); + async clickAssetPickerButton() { + await this.driver.clickElement(this.assetPickerButton); + } + + async clickSecondTokenListButton() { + const elements = await this.driver.findElements(this.tokenListButton); + await elements[1].click(); } async fillAmount(amount: string): Promise { @@ -79,31 +74,35 @@ class SendTokenPage { ); } - async goToNextScreen(): Promise { - await this.driver.clickElement(this.continueButton); + async fillNFTAmount(amount: string) { + await this.driver.pasteIntoField(this.inputNFTAmount, amount); } /** - * Verifies that an ENS domain correctly resolves to the specified Ethereum address on the send token screen. + * Fill recipient address input on send token screen. * - * @param ensDomain - The ENS domain name expected to resolve (e.g., "test.eth"). - * @param address - The Ethereum address to which the ENS domain is expected to resolve. - * @returns A promise that resolves if the ENS domain successfully resolves to the specified address on send token screen. + * @param recipientAddress - The recipient address to fill in the input field. */ - async check_ensAddressResolution( - ensDomain: string, - address: string, - ): Promise { + async fillRecipient(recipientAddress: string): Promise { console.log( - `Check ENS domain resolution: '${ensDomain}' should resolve to address '${address}' on the send token screen.`, + `Fill recipient input with ${recipientAddress} on send token screen`, ); - // check if ens domain is resolved as expected address - await this.driver.waitForSelector({ - text: ensDomain, - css: this.ensResolvedName, - }); - await this.driver.waitForSelector({ - text: address, + await this.driver.pasteIntoField(this.inputRecipient, recipientAddress); + } + + async goToNextScreen(): Promise { + await this.driver.clickElement(this.continueButton); + } + + /** + * Select recipient account on send token screen. + * + * @param recipientAccount - The recipient account to select. + */ + async selectRecipientAccount(recipientAccount: string): Promise { + await this.driver.clickElement({ + text: recipientAccount, + css: this.recipientAccount, }); } @@ -133,13 +132,28 @@ class SendTokenPage { ); } - async click_assetPickerButton() { - await this.driver.clickElement(this.assetPickerButton); - } - - async click_secondTokenListButton() { - const elements = await this.driver.findElements(this.tokenListButton); - await elements[1].click(); + /** + * Verifies that an ENS domain correctly resolves to the specified Ethereum address on the send token screen. + * + * @param ensDomain - The ENS domain name expected to resolve (e.g., "test.eth"). + * @param address - The Ethereum address to which the ENS domain is expected to resolve. + * @returns A promise that resolves if the ENS domain successfully resolves to the specified address on send token screen. + */ + async check_ensAddressResolution( + ensDomain: string, + address: string, + ): Promise { + console.log( + `Check ENS domain resolution: '${ensDomain}' should resolve to address '${address}' on the send token screen.`, + ); + // check if ens domain is resolved as expected address + await this.driver.waitForSelector({ + text: ensDomain, + css: this.ensResolvedName, + }); + await this.driver.waitForSelector({ + text: address, + }); } } diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 547f9e43a34e..c029e34efc7e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -9,6 +9,11 @@ class SettingsPage { css: '.tab-bar__tab__content__title', }; + private readonly developerOptionsButton: object = { + text: 'Developer Options', + css: '.tab-bar__tab__content__title', + }; + private readonly settingsPageTitle: object = { text: 'Settings', css: 'h3', @@ -32,6 +37,11 @@ class SettingsPage { console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } + + async goToDevelopOptionSettings(): Promise { + console.log('Navigating to Develop options page'); + await this.driver.clickElement(this.developerOptionsButton); + } } export default SettingsPage; diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index b9487ee599b9..afff2f37e57e 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -34,6 +34,20 @@ class TestDapp { tag: 'button', }; + private readonly simpleSendButton = '#sendButton'; + + private readonly erc721MintButton = '#mintButton'; + + private readonly erc721TransferFromButton = '#transferFromButton'; + + private readonly erc1155TokenIDInput = '#batchMintTokenIds'; + + private readonly erc1155TokenAmountInput = '#batchMintIdAmounts'; + + private readonly erc1155MintButton = '#batchMintButton'; + + private readonly erc1155WatchButton = '#watchAssetButton'; + private readonly erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; @@ -174,6 +188,34 @@ class TestDapp { }); } + async clickSimpleSendButton() { + await this.driver.clickElement(this.simpleSendButton); + } + + async clickERC721MintButton() { + await this.driver.clickElement(this.erc721MintButton); + } + + async clickERC721TransferFromButton() { + await this.driver.clickElement(this.erc721TransferFromButton); + } + + async fillERC1155TokenID(tokenID: string) { + await this.driver.pasteIntoField(this.erc1155TokenIDInput, tokenID); + } + + async fillERC1155TokenAmount(amount: string) { + await this.driver.pasteIntoField(this.erc1155TokenAmountInput, amount); + } + + async clickERC1155MintButton() { + await this.driver.clickElement(this.erc1155MintButton); + } + + async clickERC1155WatchButton() { + await this.driver.clickElement(this.erc1155WatchButton); + } + async clickERC721SetApprovalForAllButton() { await this.driver.clickElement(this.erc721SetApprovalForAllButton); } diff --git a/test/e2e/phishing-warning-page-server.js b/test/e2e/phishing-warning-page-server.js index 2f6099e1a3d0..a66c9211ed83 100644 --- a/test/e2e/phishing-warning-page-server.js +++ b/test/e2e/phishing-warning-page-server.js @@ -13,7 +13,7 @@ const phishingWarningDirectory = path.resolve( class PhishingWarningPageServer { constructor() { - this._server = createStaticServer(phishingWarningDirectory); + this._server = createStaticServer({ public: phishingWarningDirectory }); } async start({ port = 9999 } = {}) { diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 846acc8164cd..7a687ec254c0 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -36,7 +36,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false + "smartTransactionsOptInStatus": true }, "theme": "light", "useBlockie": false, diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 7fdbf5e1fd24..30037479fca8 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.15.2', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.3', }; diff --git a/test/e2e/snaps/test-snap-bip-32.spec.js b/test/e2e/snaps/test-snap-bip-32.spec.js index 59a9c14833e8..d52b92f0fe97 100644 --- a/test/e2e/snaps/test-snap-bip-32.spec.js +++ b/test/e2e/snaps/test-snap-bip-32.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,14 +27,19 @@ describe('Test Snap bip-32', function () { tag: 'h2', }); - // find and scroll to the bip32 test and connect + // find and scroll to the bip32 snap const snapButton1 = await driver.findElement('#connectbip32'); await driver.scrollToElement(snapButton1); + + // added delay for firefox (deflake) + await driver.delayFirefox(3000); + + // wait for and click connect to bip-32 await driver.waitForSelector('#connectbip32'); await driver.clickElement('#connectbip32'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Connect', tag: 'button', @@ -45,10 +49,13 @@ describe('Test Snap bip-32', function () { tag: 'button', }); + // wait for confirm to appear await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -64,8 +71,9 @@ describe('Test Snap bip-32', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); + // wait for and click OK and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -103,20 +111,15 @@ describe('Test Snap bip-32', function () { await driver.fill('#bip32Message-secp256k1', 'foo bar'); await driver.clickElement('#sendBip32-secp256k1'); - // hit 'approve' on the signature confirmation - await switchToNotificationWindow(driver, 2); - await driver.clickElement({ + // hit 'approve' on the signature confirmation and wait for window to close + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ text: 'Approve', tag: 'button', }); // switch back to the test-snaps window - let windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // check results of the secp256k1 signature with waitForSelector await driver.waitForSelector({ @@ -133,15 +136,21 @@ describe('Test Snap bip-32', function () { await driver.fill('#bip32Message-ed25519', 'foo bar'); await driver.clickElement('#sendBip32-ed25519'); - // hit 'approve' on the custom confirm - await switchToNotificationWindow(driver, 2); - await driver.clickElement({ + // switch to dialog window + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click 'approve' and wait for window to close + await driver.waitForSelector({ + text: 'Approve', + tag: 'button', + }); + await driver.clickElementAndWaitForWindowToClose({ text: 'Approve', tag: 'button', }); - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); - await driver.switchToWindow(windowHandles[0]); + // switch back to test-snaps window + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // check results of ed25519 signature with waitForSelector await driver.waitForSelector({ diff --git a/test/e2e/snaps/test-snap-bip-44.spec.js b/test/e2e/snaps/test-snap-bip-44.spec.js index ef70e6fc193a..8efb626045a8 100644 --- a/test/e2e/snaps/test-snap-bip-44.spec.js +++ b/test/e2e/snaps/test-snap-bip-44.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,14 +27,19 @@ describe('Test Snap bip-44', function () { tag: 'h2', }); - // find and scroll to the bip44 test and connect + // find and scroll to the bip44 snap const snapButton1 = await driver.findElement('#connectbip44'); await driver.scrollToElement(snapButton1); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect await driver.waitForSelector('#connectbip44'); await driver.clickElement('#connectbip44'); // switch to metamask extension and click connect and approve - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -59,9 +63,9 @@ describe('Test Snap bip-44', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); - // deal with OK button + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -91,20 +95,21 @@ describe('Test Snap bip-44', function () { await driver.waitForSelector('#signBip44Message'); await driver.clickElement('#signBip44Message'); - // Switch to approve signature message window and approve - await switchToNotificationWindow(driver, 2); - await driver.clickElement({ + // Switch to approve signature message window + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click approve and wait for window to close + await driver.waitForSelector({ + text: 'Approve', + tag: 'button', + }); + await driver.clickElementAndWaitForWindowToClose({ text: 'Approve', tag: 'button', }); // switch back to test-snaps page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // check the results of the message signature using waitForSelector await driver.waitForSelector({ diff --git a/test/e2e/snaps/test-snap-clientstatus.spec.js b/test/e2e/snaps/test-snap-clientstatus.spec.js index edcc48b9b137..2057f7499546 100644 --- a/test/e2e/snaps/test-snap-clientstatus.spec.js +++ b/test/e2e/snaps/test-snap-clientstatus.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,28 +27,40 @@ describe('Test Snap Client Status', function () { tag: 'h2', }); + // scroll to and click connect to client-status snap const snapButton = await driver.findElement('#connectclient-status'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectclient-status'); await driver.clickElement('#connectclient-status'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -77,7 +88,7 @@ describe('Test Snap Client Status', function () { WINDOW_TITLES.ExtensionInFullScreenView, ); - // click on the global action menu + // wait for and click on the global action menu await driver.waitForSelector( '[data-testid="account-options-menu-button"]', ); diff --git a/test/e2e/snaps/test-snap-cronjob.spec.js b/test/e2e/snaps/test-snap-cronjob.spec.js index b19de4246ac3..6f4e05883943 100644 --- a/test/e2e/snaps/test-snap-cronjob.spec.js +++ b/test/e2e/snaps/test-snap-cronjob.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,28 +27,40 @@ describe('Test Snap Cronjob', function () { tag: 'h2', }); + // scroll to and connect to cronjobs snap const snapButton = await driver.findElement('#connectcronjobs'); await driver.scrollToElement(snapButton); - await driver.delay(500); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectcronjobs'); await driver.clickElement('#connectcronjobs'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -64,7 +75,7 @@ describe('Test Snap Cronjob', function () { }); // switch to dialog popup, wait for a maximum of 65 seconds - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // look for the dialog popup to verify cronjob fired await driver.waitForSelector({ @@ -72,8 +83,8 @@ describe('Test Snap Cronjob', function () { text: 'This dialog was triggered by a cronjob', }); - // try to click on the Ok button and pass test if it works - await driver.clickElement({ + // try to click on the Ok button and pass test if window closes + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-dialog.spec.js b/test/e2e/snaps/test-snap-dialog.spec.js index 3b82781321da..36c3965aaca3 100644 --- a/test/e2e/snaps/test-snap-dialog.spec.js +++ b/test/e2e/snaps/test-snap-dialog.spec.js @@ -27,27 +27,39 @@ describe('Test Snap Dialog', function () { tag: 'h2', }); + // scroll to connect dialogs snap const dialogButton = await driver.findElement('#connectdialogs'); await driver.scrollToElement(dialogButton); - await driver.delay(500); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectdialogs'); await driver.clickElement('#connectdialogs'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', @@ -77,7 +89,7 @@ describe('Test Snap Dialog', function () { text: 'It has a single button: "OK"', }); - // click ok button + // click ok button and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', @@ -99,7 +111,7 @@ describe('Test Snap Dialog', function () { // switch to dialog popup await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - // click reject + // click reject and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'Reject', tag: 'button', @@ -120,7 +132,7 @@ describe('Test Snap Dialog', function () { // switch to dialog popup await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - // click accept + // click accept and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'Approve', tag: 'button', @@ -142,7 +154,7 @@ describe('Test Snap Dialog', function () { // switch to dialog popup await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - // click cancel button + // click cancel button and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'Cancel', tag: 'button', @@ -166,7 +178,7 @@ describe('Test Snap Dialog', function () { // fill '2323' in form field await driver.pasteIntoField('.mm-input', '2323'); - // click submit button + // click submit button and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'Submit', tag: 'button', @@ -188,7 +200,7 @@ describe('Test Snap Dialog', function () { // switch to dialog popup await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - // click cancel button + // click cancel button and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'Cancel', tag: 'span', @@ -212,7 +224,7 @@ describe('Test Snap Dialog', function () { // fill '2323' in form field await driver.pasteIntoField('#custom-input', '2323'); - // click confirm button + // click confirm button and wait for window to close await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'span', diff --git a/test/e2e/snaps/test-snap-ethprovider.spec.js b/test/e2e/snaps/test-snap-ethprovider.spec.js index feee8c76783a..0e5ea68a09eb 100644 --- a/test/e2e/snaps/test-snap-ethprovider.spec.js +++ b/test/e2e/snaps/test-snap-ethprovider.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,30 +27,42 @@ describe('Test Snap ethereum_provider', function () { tag: 'h2', }); + // scroll to ethereum provider snap const snapButton = await driver.findElement( '#connectethereum-provider', ); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectethereum-provider'); await driver.clickElement('#connectethereum-provider'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -85,17 +96,19 @@ describe('Test Snap ethereum_provider', function () { await driver.delay(500); await driver.clickElement('#sendEthproviderAccounts'); - // switch to metamask window and click through confirmations - await switchToNotificationWindow(driver, 2); + // switch to metamask window + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Next', tag: 'button', }); + + // wait for and click confirm and wait for window to close await driver.waitForSelector({ text: 'Confirm', tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-get-file.spec.js b/test/e2e/snaps/test-snap-get-file.spec.js index ce7f29fe3bf2..386df0fa56ad 100644 --- a/test/e2e/snaps/test-snap-get-file.spec.js +++ b/test/e2e/snaps/test-snap-get-file.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,28 +27,40 @@ describe('Test Snap Get File', function () { tag: 'h2', }); - const dialogButton = await driver.findElement('#connectgetfile'); - await driver.scrollToElement(dialogButton); - await driver.delay(1000); + // scroll to and wait for connect to get file button + const snapButton = await driver.findElement('#connectgetfile'); + await driver.scrollToElement(snapButton); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectgetfile'); await driver.clickElement('#connectgetfile'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-get-locale.spec.js b/test/e2e/snaps/test-snap-get-locale.spec.js index 9ccd4f3c141f..a87d965c6170 100644 --- a/test/e2e/snaps/test-snap-get-locale.spec.js +++ b/test/e2e/snaps/test-snap-get-locale.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,13 +27,25 @@ describe('Test Snap Get Locale', function () { tag: 'h2', }); + // scroll to dialog snap const dialogButton = await driver.findElement('#connectgetlocale'); await driver.scrollToElement(dialogButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectgetlocale'); await driver.clickElement('#connectgetlocale'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -46,18 +57,21 @@ describe('Test Snap Get Locale', function () { tag: 'p', }); + // wait for confirm await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-getentropy.spec.js b/test/e2e/snaps/test-snap-getentropy.spec.js index 7815f7329878..b25fcfbc10f6 100644 --- a/test/e2e/snaps/test-snap-getentropy.spec.js +++ b/test/e2e/snaps/test-snap-getentropy.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,30 +27,45 @@ describe('Test Snap getEntropy', function () { tag: 'h2', }); + // scroll to get entropy snap const snapButton = await driver.findElement('#connectGetEntropySnap'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectGetEntropySnap'); await driver.clickElement('#connectGetEntropySnap'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm selector await driver.waitForSelector({ text: 'Confirm' }); + // dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm button await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -72,9 +86,15 @@ describe('Test Snap getEntropy', function () { await driver.delay(500); await driver.clickElement('#signEntropyMessage'); - // Switch to approve signature message window and approve - await switchToNotificationWindow(driver, 2); - await driver.clickElement({ + // Switch to approve signature message window + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click on approve and wait for window to close + await driver.waitForSelector({ + text: 'Approve', + tag: 'button', + }); + await driver.clickElementAndWaitForWindowToClose({ text: 'Approve', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-homepage.spec.js b/test/e2e/snaps/test-snap-homepage.spec.js index 8f1e851d0a0d..fb5dc575a0de 100644 --- a/test/e2e/snaps/test-snap-homepage.spec.js +++ b/test/e2e/snaps/test-snap-homepage.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,29 +27,40 @@ describe('Test Snap Homepage', function () { tag: 'h2', }); - // find and scroll to the honmepage test and connect + // find and scroll to the homepage snap const snapButton1 = await driver.findElement('#connecthomepage'); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connecthomepage'); await driver.clickElement('#connecthomepage'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for windows to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-installed.spec.js b/test/e2e/snaps/test-snap-installed.spec.js index 5c7a3394966f..11325920c723 100644 --- a/test/e2e/snaps/test-snap-installed.spec.js +++ b/test/e2e/snaps/test-snap-installed.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -33,28 +32,40 @@ describe('Test Snap Installed', function () { tag: 'h2', }); + // scroll to dialogs snap const confirmButton = await driver.findElement('#connectdialogs'); await driver.scrollToElement(confirmButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectdialogs'); await driver.clickElement('#connectdialogs'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -71,26 +82,37 @@ describe('Test Snap Installed', function () { // click to connect to errors snap const errorButton = await driver.findElement('#connecterrors'); await driver.scrollToElement(errorButton); - await driver.delay(500); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connecterrors'); await driver.clickElement('#connecterrors'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-interactive-ui.spec.js b/test/e2e/snaps/test-snap-interactive-ui.spec.js index 200c1916ddaa..a2d00b1348cc 100644 --- a/test/e2e/snaps/test-snap-interactive-ui.spec.js +++ b/test/e2e/snaps/test-snap-interactive-ui.spec.js @@ -21,14 +21,32 @@ describe('Test Snap Interactive UI', function () { // navigate to test snaps page and connect to interactive ui snap await driver.openNewPage(TEST_SNAPS_WEBSITE_URL); - await driver.delay(1000); + + // wait for page to load + await driver.waitForSelector({ + text: 'Installed Snaps', + tag: 'h2', + }); + + // scroll to interactive-ui snap const dialogButton = await driver.findElement('#connectinteractive-ui'); await driver.scrollToElement(dialogButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectinteractive-ui'); await driver.clickElement('#connectinteractive-ui'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -47,9 +65,9 @@ describe('Test Snap Interactive UI', function () { tag: 'button', }); - // wait for anc click OK + // wait for and click OK and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -93,8 +111,10 @@ describe('Test Snap Interactive UI', function () { await driver.waitForSelector({ text: 'option3', tag: 'p' }); await driver.waitForSelector({ text: 'true', tag: 'p' }); - // try to click on approve - await driver.clickElement('[data-testid="confirmation-submit-button"]'); + // click on approve and wait for window to close + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirmation-submit-button"]', + ); // switch to test snaps tab await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); diff --git a/test/e2e/snaps/test-snap-jsx.spec.js b/test/e2e/snaps/test-snap-jsx.spec.js index b5f651125232..4bdb94990df7 100644 --- a/test/e2e/snaps/test-snap-jsx.spec.js +++ b/test/e2e/snaps/test-snap-jsx.spec.js @@ -27,15 +27,21 @@ describe('Test Snap JSX', function () { tag: 'h2', }); - // find and scroll to the jsx test and connect + // find and scroll to the jsx test const snapButton = await driver.findElement('#connectjsx'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect await driver.waitForSelector('#connectjsx'); await driver.clickElement('#connectjsx'); - // switch to dialog window and click connect + // switch to dialog window await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect await driver.waitForSelector({ text: 'Connect', tag: 'button', @@ -45,17 +51,20 @@ describe('Test Snap JSX', function () { tag: 'button', }); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok await driver.waitForSelector({ text: 'OK' }); - await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', diff --git a/test/e2e/snaps/test-snap-lifecycle.spec.js b/test/e2e/snaps/test-snap-lifecycle.spec.js index 40387376557c..e84845ec2553 100644 --- a/test/e2e/snaps/test-snap-lifecycle.spec.js +++ b/test/e2e/snaps/test-snap-lifecycle.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,27 +27,39 @@ describe('Test Snap Lifecycle Hooks', function () { tag: 'h2', }); + // scroll to lifecycle hooks snap const snapButton = await driver.findElement('#connectlifecycle-hooks'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectlifecycle-hooks'); await driver.clickElement('#connectlifecycle-hooks'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); - await driver.waitForSelector({ text: 'OK' }); - + // wait for and click ok + await driver.waitForSelector({ text: 'OK', tag: 'button' }); await driver.clickElement({ text: 'OK', tag: 'button', @@ -64,7 +75,7 @@ describe('Test Snap Lifecycle Hooks', function () { }); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // check dialog contents const result = await driver.findElement('.snap-ui-renderer__panel'); diff --git a/test/e2e/snaps/test-snap-management.spec.js b/test/e2e/snaps/test-snap-management.spec.js index 7e62311af6bc..fd11e801a061 100644 --- a/test/e2e/snaps/test-snap-management.spec.js +++ b/test/e2e/snaps/test-snap-management.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,29 +27,40 @@ describe('Test Snap Management', function () { tag: 'h2', }); - // find and scroll to the notifications card and click first + // find and scroll to the notifications snap const snapButton = await driver.findElement('#connectnotifications'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnotifications'); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-managestate.spec.js b/test/e2e/snaps/test-snap-managestate.spec.js index b7bfdb7678d4..4d6a58faa678 100644 --- a/test/e2e/snaps/test-snap-managestate.spec.js +++ b/test/e2e/snaps/test-snap-managestate.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,29 +27,40 @@ describe('Test Snap manageState', function () { tag: 'h2', }); - // navigate to test snaps page and connect to manage-state snap + // scroll to manage-state snap const snapButton1 = await driver.findElement('#connectmanage-state'); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectmanage-state'); await driver.clickElement('#connectmanage-state'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -64,6 +74,7 @@ describe('Test Snap manageState', function () { text: 'Reconnect to Manage State Snap', }); + // enter data and click send managestate await driver.pasteIntoField('#dataManageState', '23'); const snapButton2 = await driver.findElement( '#retrieveManageStateResult', diff --git a/test/e2e/snaps/test-snap-metrics.spec.js b/test/e2e/snaps/test-snap-metrics.spec.js index a26671ca4261..54ebd572d993 100644 --- a/test/e2e/snaps/test-snap-metrics.spec.js +++ b/test/e2e/snaps/test-snap-metrics.spec.js @@ -192,29 +192,40 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the notifications card and click first + // find and scroll to the notifications snap const snapButton = await driver.findElement('#connectnotifications'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnotifications'); await driver.clickElement('#connectnotifications'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -277,21 +288,31 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the notifications card and click first + // find and scroll to the notifications snap const snapButton = await driver.findElement('#connectnotifications'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnotifications'); await driver.clickElement('#connectnotifications'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); - + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Cancel', tag: 'button', @@ -355,19 +376,31 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the notifications card and click first + // find and scroll to the notifications snap const snapButton = await driver.findElement('#connectnotifications'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnotifications'); await driver.clickElement('#connectnotifications'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for connection failure await driver.waitForSelector({ text: 'Connection failed' }); // check that snap installed event metrics have been sent @@ -424,29 +457,40 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the notifications card and click first + // find and scroll to the notifications snap const snapButton = await driver.findElement('#connectnotifications'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnotifications'); await driver.clickElement('#connectnotifications'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -542,19 +586,31 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the correct card and connect to update snap + // find and scroll to the update snap const snapButton = await driver.findElement('#connectUpdate'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdate'); await driver.clickElement('#connectUpdate'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); // Wait for the permissions content to be rendered @@ -563,8 +619,10 @@ describe('Test Snap Metrics', function () { tag: 'span', }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm button await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -580,9 +638,9 @@ describe('Test Snap Metrics', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -596,27 +654,35 @@ describe('Test Snap Metrics', function () { text: 'Reconnect to Update Snap', }); - // find and scroll to the correct card and click first + // find and scroll to the update new button const snapButton2 = await driver.findElement('#connectUpdateNew'); await driver.scrollToElement(snapButton2); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdateNew'); await driver.clickElement('#connectUpdateNew'); // switch to metamask popup and update await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-update-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -690,19 +756,31 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the correct card and connect to update snap + // find and scroll to the update snap const snapButton = await driver.findElement('#connectUpdate'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdate'); await driver.clickElement('#connectUpdate'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); // Wait for the permissions content to be rendered @@ -711,8 +789,10 @@ describe('Test Snap Metrics', function () { tag: 'span', }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -728,9 +808,9 @@ describe('Test Snap Metrics', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -744,20 +824,28 @@ describe('Test Snap Metrics', function () { text: 'Reconnect to Update Snap', }); - // find and scroll to the correct card and click first + // find and scroll to the update snap const snapButton2 = await driver.findElement('#connectUpdateNew'); await driver.scrollToElement(snapButton2); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect new + await driver.waitForSelector('#connectUpdateNew'); await driver.clickElement('#connectUpdateNew'); // switch to metamask popup and update await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-update-scroll"]'); - await driver.clickElement({ + // click cancel and wait for window to close + await driver.clickElementAndWaitForWindowToClose({ text: 'Cancel', tag: 'button', }); @@ -826,19 +914,31 @@ describe('Test Snap Metrics', function () { tag: 'h2', }); - // find and scroll to the correct card and connect to update snap + // find and scroll to the update snap const snapButton = await driver.findElement('#connectUpdate'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdate'); await driver.clickElement('#connectUpdate'); - // switch to metamask popup and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); // Wait for the permissions content to be rendered @@ -847,8 +947,10 @@ describe('Test Snap Metrics', function () { tag: 'span', }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -864,9 +966,9 @@ describe('Test Snap Metrics', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -880,18 +982,25 @@ describe('Test Snap Metrics', function () { text: 'Reconnect to Update Snap', }); - // find and scroll to the correct card and click first + // find and scroll to the update snap const snapButton2 = await driver.findElement('#connectUpdateNew'); await driver.scrollToElement(snapButton2); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click update new + await driver.waitForSelector('#connectUpdateNew'); await driver.clickElement('#connectUpdateNew'); + // wait for and close alert window await driver.delay(1000); await driver.closeAlertPopup(); // switch to metamask popup and update await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + // wait for failure message await driver.waitForSelector({ text: 'Update failed' }); // check that snap updated event metrics have been sent diff --git a/test/e2e/snaps/test-snap-multi-install.spec.js b/test/e2e/snaps/test-snap-multi-install.spec.js index d30b1307d216..2b48a1bb86ed 100644 --- a/test/e2e/snaps/test-snap-multi-install.spec.js +++ b/test/e2e/snaps/test-snap-multi-install.spec.js @@ -21,14 +21,32 @@ describe('Test Snap Multi Install', function () { // navigate to test snaps page and multi-install snaps await driver.openNewPage(TEST_SNAPS_WEBSITE_URL); - await driver.delay(1000); + + // wait for page to load + await driver.waitForSelector({ + text: 'Installed Snaps', + tag: 'h2', + }); + + // scroll to multi-install snap const dialogButton = await driver.findElement('#multi-install-connect'); await driver.scrollToElement(dialogButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#multi-install-connect'); await driver.clickElement('#multi-install-connect'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -57,7 +75,7 @@ describe('Test Snap Multi Install', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); - // wait for anc click OK + // wait for and click OK await driver.waitForSelector({ text: 'OK' }); await driver.clickElement({ text: 'OK', @@ -90,7 +108,7 @@ describe('Test Snap Multi Install', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); - // wait for anc click OK + // wait for and click OK await driver.waitForSelector({ text: 'OK' }); await driver.clickElement({ text: 'OK', diff --git a/test/e2e/snaps/test-snap-namelookup.spec.js b/test/e2e/snaps/test-snap-namelookup.spec.js index 94af575deb9c..c7a61bb928fa 100644 --- a/test/e2e/snaps/test-snap-namelookup.spec.js +++ b/test/e2e/snaps/test-snap-namelookup.spec.js @@ -2,7 +2,6 @@ const { withFixtures, defaultGanacheOptions, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,28 +27,39 @@ describe('Test Snap Name Lookup', function () { tag: 'h2', }); - // find and scroll to the namelookup test snap and connect + // find and scroll to the namelookup test snap const snapButton1 = await driver.findElement('#connectname-lookup'); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectname-lookup'); await driver.clickElement('#connectname-lookup'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', diff --git a/test/e2e/snaps/test-snap-networkaccess.spec.js b/test/e2e/snaps/test-snap-networkaccess.spec.js index e3fa954a79c2..a8bfb9141394 100644 --- a/test/e2e/snaps/test-snap-networkaccess.spec.js +++ b/test/e2e/snaps/test-snap-networkaccess.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,28 +27,40 @@ describe('Test Snap networkAccess', function () { tag: 'h2', }); + // scroll to network access snap const dialogButton = await driver.findElement('#connectnetwork-access'); await driver.scrollToElement(dialogButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnetwork-access'); await driver.clickElement('#connectnetwork-access'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-notification.spec.js b/test/e2e/snaps/test-snap-notification.spec.js index 7c9fdede898d..c96c1bc96e3d 100644 --- a/test/e2e/snaps/test-snap-notification.spec.js +++ b/test/e2e/snaps/test-snap-notification.spec.js @@ -2,14 +2,13 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); describe('Test Snap Notification', function () { - it('can send 1 correctly read inapp notification', async function () { + it('can send 1 correctly read in-app notification', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), @@ -28,29 +27,40 @@ describe('Test Snap Notification', function () { tag: 'h2', }); - // connect to notifications snap + // scroll to notifications snap const snapButton = await driver.findElement('#connectnotifications'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectnotifications'); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to cloe await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -64,6 +74,7 @@ describe('Test Snap Notification', function () { text: 'Reconnect to Notifications Snap', }); + // click to send notification await driver.clickElement('#sendInAppNotification'); // switch back to the extension page diff --git a/test/e2e/snaps/test-snap-revoke-perm.spec.js b/test/e2e/snaps/test-snap-revoke-perm.spec.js index b70cc3ab4cc1..e61c1d831862 100644 --- a/test/e2e/snaps/test-snap-revoke-perm.spec.js +++ b/test/e2e/snaps/test-snap-revoke-perm.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, WINDOW_TITLES, - switchToNotificationWindow, unlockWallet, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,30 +27,42 @@ describe('Test Snap revoke permission', function () { tag: 'h2', }); + // scroll to ethereum-provider snap const snapButton = await driver.findElement( '#connectethereum-provider', ); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectethereum-provider'); await driver.clickElement('#connectethereum-provider'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 3); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click connect await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -70,17 +81,36 @@ describe('Test Snap revoke permission', function () { '#sendEthproviderAccounts', ); await driver.scrollToElement(snapButton3); - await driver.delay(500); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click send + await driver.waitForSelector('#sendEthproviderAccounts'); await driver.clickElement('#sendEthproviderAccounts'); - // switch to metamask window and click through confirmations - await switchToNotificationWindow(driver, 3); + // switch to metamask window + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click next + await driver.waitForSelector({ + text: 'Next', + tag: 'button', + }); await driver.clickElement({ text: 'Next', tag: 'button', }); + + // delay added for rendering time (deflake) await driver.delay(500); - await driver.clickElement({ + + // wait for and click confirm and wait for window to close + await driver.waitForSelector({ + text: 'Confirm', + tag: 'button', + }); + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', }); @@ -98,6 +128,8 @@ describe('Test Snap revoke permission', function () { await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); + + // added delay for rendering (deflake) await driver.delay(1000); // click on the global action menu @@ -134,18 +166,39 @@ describe('Test Snap revoke permission', function () { '#sendEthproviderAccounts', ); await driver.scrollToElement(snapButton4); - await driver.delay(500); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#sendEthproviderAccounts'); await driver.clickElement('#sendEthproviderAccounts'); - // switch to metamask window and click through confirmations + // delay added for rendering time (deflake) await driver.delay(500); - await switchToNotificationWindow(driver, 3); + + // switch to metamask dialog + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click next + await driver.waitForSelector({ + text: 'Next', + tag: 'button', + }); await driver.clickElement({ text: 'Next', tag: 'button', }); + + // delay added for rendering time (deflake) await driver.delay(500); - await driver.clickElement({ + + // wait for and click confirm and wait for window to close + await driver.waitForSelector({ + text: 'Confirm', + tag: 'button', + }); + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', }); diff --git a/test/e2e/snaps/test-snap-rpc.spec.js b/test/e2e/snaps/test-snap-rpc.spec.js index b6ad95c89c65..2b16a9f0929a 100644 --- a/test/e2e/snaps/test-snap-rpc.spec.js +++ b/test/e2e/snaps/test-snap-rpc.spec.js @@ -1,7 +1,6 @@ const { defaultGanacheOptions, withFixtures, - switchToNotificationWindow, unlockWallet, WINDOW_TITLES, } = require('../helpers'); @@ -28,23 +27,37 @@ describe('Test Snap RPC', function () { tag: 'h2', }); - // find and scroll to the bip32 test and connect + // find and scroll to the bip32 test snap const snapButton1 = await driver.findElement('#connectbip32'); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectbip32'); await driver.clickElement('#connectbip32'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm button await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -60,10 +73,9 @@ describe('Test Snap RPC', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); - // deal with OK button + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -71,31 +83,45 @@ describe('Test Snap RPC', function () { // switch back to test-snaps window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + // scroll to json-rpc snap const snapButton2 = await driver.findElement('#connectjson-rpc'); await driver.scrollToElement(snapButton2); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectjson-rpc'); await driver.clickElement('#connectjson-rpc'); - await switchToNotificationWindow(driver, 2); + // switch to metamask dialog + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); + // switch to test snaps window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success @@ -104,10 +130,15 @@ describe('Test Snap RPC', function () { text: 'Reconnect to JSON-RPC Snap', }); - // click send inputs on test snap page + // scroll to send rpc const snapButton3 = await driver.findElement('#sendRpc'); await driver.scrollToElement(snapButton3); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click send + await driver.waitForSelector('#sendRpc'); await driver.clickElement('#sendRpc'); // check result with waitForSelector diff --git a/test/e2e/snaps/test-snap-siginsights.spec.js b/test/e2e/snaps/test-snap-siginsights.spec.js index b72d6e248ff0..94237b99c7f9 100644 --- a/test/e2e/snaps/test-snap-siginsights.spec.js +++ b/test/e2e/snaps/test-snap-siginsights.spec.js @@ -27,32 +27,48 @@ describe('Test Snap Signature Insights', function () { // navigate to test snaps page and connect await driver.openNewPage(TEST_SNAPS_WEBSITE_URL); - await driver.delay(1000); - // find and scroll to the transaction-insights test and connect + // wait for page to load + await driver.waitForSelector({ + text: 'Installed Snaps', + tag: 'h2', + }); + + // find and scroll to the transaction-insights snap const snapButton1 = await driver.findElement( '#connectsignature-insights', ); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectsignature-insights'); await driver.clickElement('#connectsignature-insights'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', @@ -129,7 +145,7 @@ describe('Test Snap Signature Insights', function () { tag: 'p', }); - // click sign button + // click sign button and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-button"]', ); @@ -223,7 +239,7 @@ describe('Test Snap Signature Insights', function () { tag: 'p', }); - // click sign button + // click sign button and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-button"]', ); @@ -257,32 +273,45 @@ describe('Test Snap Signature Insights', function () { // navigate to test snaps page and connect await driver.openNewPage(TEST_SNAPS_WEBSITE_URL); + + // delay added for page render (deflake) await driver.delay(1000); - // find and scroll to the transaction-insights test and connect + // find and scroll to the transaction-insights test snap const snapButton1 = await driver.findElement( '#connectsignature-insights', ); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectsignature-insights'); await driver.clickElement('#connectsignature-insights'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', @@ -320,10 +349,9 @@ describe('Test Snap Signature Insights', function () { text: '127.0.0.1:8080', tag: 'span', }); - await driver.clickElement('.mm-checkbox__input-wrapper'); - // click sign button + // click sign button and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="snapInsightsButtonConfirm"]', ); @@ -363,10 +391,9 @@ describe('Test Snap Signature Insights', function () { text: '127.0.0.1:8080', tag: 'span', }); - await driver.clickElement('.mm-checkbox__input-wrapper'); - // click sign button + // click sign button and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="snapInsightsButtonConfirm"]', ); @@ -410,10 +437,9 @@ describe('Test Snap Signature Insights', function () { text: '127.0.0.1:8080', tag: 'span', }); - await driver.clickElement('.mm-checkbox__input-wrapper'); - // click sign button + // click sign button and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="snapInsightsButtonConfirm"]', ); @@ -457,10 +483,9 @@ describe('Test Snap Signature Insights', function () { text: '127.0.0.1:8080', tag: 'span', }); - await driver.clickElement('.mm-checkbox__input-wrapper'); - // click sign button + // click sign button and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="snapInsightsButtonConfirm"]', ); diff --git a/test/e2e/snaps/test-snap-txinsights-v2.spec.js b/test/e2e/snaps/test-snap-txinsights-v2.spec.js index 830629d1c43e..a249e9daa79b 100644 --- a/test/e2e/snaps/test-snap-txinsights-v2.spec.js +++ b/test/e2e/snaps/test-snap-txinsights-v2.spec.js @@ -27,37 +27,61 @@ describe('Test Snap TxInsights-v2', function () { tag: 'h2', }); - // find and scroll to the transaction-insights test and connect + // find and scroll to the transaction-insights test snap const snapButton1 = await driver.findElement( '#connecttransaction-insights', ); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connecttransaction-insights'); await driver.clickElement('#connecttransaction-insights'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click connect + await driver.waitForSelector({ text: 'Confirm' }); await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close + await driver.waitForSelector({ text: 'OK' }); await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); - // switch to test-snaps page and get accounts + // switch to test-snaps page await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // wait for and click get accounts + await driver.waitForSelector('#getAccounts'); await driver.clickElement('#getAccounts'); - // switch back to MetaMask window and deal with dialogs + // switch back to MetaMask window await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click confirm and wait for window to close + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', @@ -67,15 +91,19 @@ describe('Test Snap TxInsights-v2', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); await driver.clickElement('#sendInsights'); - // switch back to MetaMask window and switch to tx insights pane + // delay added for rendering (deflake) await driver.delay(2000); + + // switch back to MetaMask window and switch to tx insights pane await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + // find confirm button await driver.findClickableElement({ text: 'Confirm', tag: 'button', }); + // wait for and click insights snap tab await driver.waitForSelector({ text: 'Insights Example Snap', tag: 'button', @@ -119,10 +147,12 @@ describe('Test Snap TxInsights-v2', function () { tag: 'button', }); - // switch back to MetaMask tab and switch to activity pane + // switch back to MetaMask tab await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); + + // switch to activity pane await driver.clickElement({ tag: 'button', text: 'Activity', diff --git a/test/e2e/snaps/test-snap-txinsights.spec.js b/test/e2e/snaps/test-snap-txinsights.spec.js index 7f6b7a3bec46..21feafd06cb9 100644 --- a/test/e2e/snaps/test-snap-txinsights.spec.js +++ b/test/e2e/snaps/test-snap-txinsights.spec.js @@ -27,26 +27,41 @@ describe('Test Snap TxInsights', function () { tag: 'h2', }); - // find and scroll to the transaction-insights test and connect + // find and scroll to the transaction-insights test snap const snapButton1 = await driver.findElement( '#connecttransaction-insights', ); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connecttransaction-insights'); await driver.clickElement('#connecttransaction-insights'); - // switch to metamask extension and click connect + // switch to metamask extension await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm + await driver.waitForSelector({ text: 'Confirm' }); await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close + await driver.waitForSelector({ text: 'OK' }); await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', @@ -54,23 +69,40 @@ describe('Test Snap TxInsights', function () { // switch to test-snaps page and get accounts await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // click get accounts await driver.clickElement('#getAccounts'); - // switch back to MetaMask window and deal with dialogs + // switch back to MetaMask window await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click next and wait for window to close + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', }); - // switch to test-snaps page and send tx + // switch to test-snaps page await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // click send tx await driver.clickElement('#sendInsights'); - // switch back to MetaMask window and switch to tx insights pane + // delay added for rendering (deflake) await driver.delay(2000); + + // switch back to MetaMask window await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + // wait for and switch to insight snap pane + await driver.waitForSelector({ + text: 'Insights Example Snap', + tag: 'button', + }); await driver.clickElement({ text: 'Insights Example Snap', tag: 'button', diff --git a/test/e2e/snaps/test-snap-ui-imgs.spec.js b/test/e2e/snaps/test-snap-ui-imgs.spec.js index 4d8f17ad852a..8a73c5723611 100644 --- a/test/e2e/snaps/test-snap-ui-imgs.spec.js +++ b/test/e2e/snaps/test-snap-ui-imgs.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,29 +27,43 @@ describe('Test Snap Images', function () { tag: 'h2', }); - // find and scroll to the images test and connect + // find and scroll to the images test snap const snapButton1 = await driver.findElement('#connectimages'); await driver.scrollToElement(snapButton1); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect await driver.waitForSelector('#connectimages'); await driver.clickElement('#connectimages'); - // switch to metamask extension and click connect and approve - await switchToNotificationWindow(driver, 2); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + + // wait for confirm await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', }); - // deal with OK button + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); await driver.clickElementAndWaitForWindowToClose({ text: 'OK', @@ -75,7 +88,7 @@ describe('Test Snap Images', function () { // check snaps ui image using waitForSelector await driver.waitForSelector('[data-testid="snaps-ui-image"]'); - // click ok to close window + // click ok to close window and wait for window to close await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirmation-submit-button"]', ); diff --git a/test/e2e/snaps/test-snap-uilinks.spec.js b/test/e2e/snaps/test-snap-uilinks.spec.js index 4e75765cbac6..8c4c397f8196 100644 --- a/test/e2e/snaps/test-snap-uilinks.spec.js +++ b/test/e2e/snaps/test-snap-uilinks.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -20,31 +19,49 @@ describe('Test Snap UI Links', function () { async ({ driver }) => { await unlockWallet(driver); - // navigate to test snaps page and connect to dialog snap + // navigate to test snaps page await driver.openNewPage(TEST_SNAPS_WEBSITE_URL); - await driver.delay(1000); + + // wait for page to load + await driver.waitForSelector({ + text: 'Installed Snaps', + tag: 'h2', + }); + + // scroll to dialogs snap const dialogButton = await driver.findElement('#connectdialogs'); await driver.scrollToElement(dialogButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectdialogs'); await driver.clickElement('#connectdialogs'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -60,10 +77,14 @@ describe('Test Snap UI Links', function () { // click conf button await driver.clickElement('#sendConfirmationButton'); + + // delay added for rendering (deflake) await driver.delay(500); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // delay added for rendering (deflake) await driver.delay(500); // wait for link to appear and click it @@ -102,7 +123,7 @@ describe('Test Snap UI Links', function () { }); // switch back to metamask window - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // wait for and click approve button await driver.waitForSelector({ diff --git a/test/e2e/snaps/test-snap-update-component.spec.js b/test/e2e/snaps/test-snap-update-component.spec.js index e471c76ad6e8..c65c9e047668 100644 --- a/test/e2e/snaps/test-snap-update-component.spec.js +++ b/test/e2e/snaps/test-snap-update-component.spec.js @@ -1,9 +1,4 @@ -const { - withFixtures, - switchToNotificationWindow, - unlockWallet, - WINDOW_TITLES, -} = require('../helpers'); +const { withFixtures, unlockWallet, WINDOW_TITLES } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); @@ -36,23 +31,37 @@ describe('Test Snap update via snaps component', function () { tag: 'h2', }); - // find and scroll to the correct card and connect to update snap + // find and scroll to the update snap const snapButton = await driver.findElement('#connectUpdate'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdate'); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 3); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm await driver.waitForSelector({ text: 'Confirm' }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -68,10 +77,9 @@ describe('Test Snap update via snaps component', function () { '[data-testid="snap-install-warning-modal-confirm"]', ); - // deal with OK button + // wait for and click OK button and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -90,7 +98,7 @@ describe('Test Snap update via snaps component', function () { WINDOW_TITLES.ExtensionInFullScreenView, ); - // click on the global action menu + // wait for and click on the global action menu await driver.waitForSelector( '[data-testid="account-options-menu-button"]', ); @@ -124,20 +132,25 @@ describe('Test Snap update via snaps component', function () { tag: 'button', }); + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-update-scroll"]'); + // wait for confirm await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // click checkbox await driver.clickElement('.mm-checkbox__input'); + + // click install warning confirm await driver.clickElement( '[data-testid="snap-install-warning-modal-confirm"]', ); + // wait for and click ok await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ text: 'OK', tag: 'button', @@ -157,17 +170,18 @@ describe('Test Snap update via snaps component', function () { text: 'Snaps', tag: 'div', }); + + // wait for and click into snap view await driver.waitForSelector({ text: 'BIP-32 Example Snap', tag: 'p', }); - - // click into snap view and attempt to update the snap await driver.clickElement({ text: 'BIP-32 Example Snap', tag: 'p', }); + // make sure update button isn't present await driver.assertElementNotPresent( { css: '.mm-button-link', diff --git a/test/e2e/snaps/test-snap-update.spec.js b/test/e2e/snaps/test-snap-update.spec.js index 6f5500d593a4..ac35b8947787 100644 --- a/test/e2e/snaps/test-snap-update.spec.js +++ b/test/e2e/snaps/test-snap-update.spec.js @@ -1,7 +1,6 @@ const { defaultGanacheOptions, withFixtures, - switchToNotificationWindow, unlockWallet, WINDOW_TITLES, } = require('../helpers'); @@ -28,24 +27,37 @@ describe('Test Snap update', function () { tag: 'h2', }); - // find and scroll to the correct card and connect to update snap + // find and scroll to the update snap const snapButton = await driver.findElement('#connectUpdate'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdate'); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for confirm await driver.waitForSelector({ text: 'Confirm' }); - // scroll to bottom + // click and dismiss possible scroll element await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click confirm await driver.clickElement({ text: 'Confirm', tag: 'button', @@ -74,22 +86,29 @@ describe('Test Snap update', function () { text: 'Reconnect to Update Snap', }); - // find and scroll to the correct card and click first + // find and scroll to the update snap const snapButton2 = await driver.findElement('#connectUpdateNew'); await driver.scrollToElement(snapButton2); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectUpdateNew'); await driver.clickElement('#connectUpdateNew'); // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Update request' }); // Scroll to bottom of dialog await driver.clickElementSafe('[data-testid="snap-update-scroll"]'); + // Click confirm button await driver.clickElementAndWaitToDisappear( '[data-testid="page-container-footer-next"]', ); + // When it is confirmed, click okay button await driver.waitForSelector({ text: 'OK' }); await driver.clickElement('[data-testid="page-container-footer-next"]'); diff --git a/test/e2e/snaps/test-snap-wasm.spec.js b/test/e2e/snaps/test-snap-wasm.spec.js index ada7d604d97a..205300c570c0 100644 --- a/test/e2e/snaps/test-snap-wasm.spec.js +++ b/test/e2e/snaps/test-snap-wasm.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,28 +27,40 @@ describe('Test Snap WASM', function () { tag: 'h2', }); + // scroll to wasm snap const snapButton = await driver.findElement('#connectwasm'); await driver.scrollToElement(snapButton); - await driver.delay(1000); + + // added delay for firefox (deflake) + await driver.delayFirefox(1000); + + // wait for and click connect + await driver.waitForSelector('#connectwasm'); await driver.clickElement('#connectwasm'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + // switch to metamask extension + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for and click connect + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); await driver.clickElement({ text: 'Connect', tag: 'button', }); + // wait for and click confirm await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); + // wait for and click ok and wait for window to close await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js deleted file mode 100644 index 04980cf20c3e..000000000000 --- a/test/e2e/tests/account/add-account.spec.js +++ /dev/null @@ -1,232 +0,0 @@ -const { strict: assert } = require('assert'); -const { - TEST_SEED_PHRASE, - withFixtures, - completeImportSRPOnboardingFlow, - sendTransaction, - findAnotherAccountFromAccountList, - locateAccountBalanceDOM, - logInWithBalanceValidation, - regularDelayMs, - unlockWallet, - WALLET_PASSWORD, - generateGanacheOptions, -} = require('../../helpers'); - -const FixtureBuilder = require('../../fixture-builder'); - -describe('Add account', function () { - const secondAccount = '0x3ED0eE22E0685Ebbf07b2360A8331693c413CC59'; - - const ganacheOptions = generateGanacheOptions({ - secretKey: - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', - }); - - it('should display correct new account name after create', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(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'); - // 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"]', - text: '2nd account', - }); - }, - ); - }); - - it('should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - await driver.navigate(); - - // On boarding with 1st account - await completeImportSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - // Check address of 1st accoun - await locateAccountBalanceDOM(driver, ganacheServer); - await driver.findElement('[data-testid="app-header-copy-button"]'); - - // 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"]', - ); - await driver.clickElement( - '[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 - await locateAccountBalanceDOM(driver); - await driver.findElement('[data-testid="app-header-copy-button"]'); - - // Log into the account with balance(account 1) - // and transfer some balance to 2nd account - // so they will not be removed after recovering SRP - const accountOneSelector = await findAnotherAccountFromAccountList( - driver, - 1, - 'Account 1', - ); - await locateAccountBalanceDOM(driver); - await driver.clickElement(accountOneSelector); - await sendTransaction(driver, secondAccount, '2.8'); - - // Lock the account - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - - await driver.clickElement('[data-testid="global-menu-lock"]'); - await driver.waitForSelector('[data-testid="unlock-page"]'); - - // Recover via SRP in "forget password" option - await driver.clickElement('.unlock-page__link'); - await driver.pasteIntoField( - '[data-testid="import-srp__srp-word-0"]', - TEST_SEED_PHRASE, - ); - await driver.fill('#password', 'correct horse battery staple'); - await driver.fill('#confirm-password', 'correct horse battery staple'); - - await driver.clickElement( - '[data-testid="create-new-vault-submit-button"]', - ); - - // Land in 1st account home page - await driver.findElement('.home__main-view'); - await locateAccountBalanceDOM(driver); - - // Check address of 1st account - await driver.findElement('[data-testid="app-header-copy-button"]'); - - // Check address of 2nd account - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement({ - css: `.multichain-account-list-item__account-name__button`, - text: 'Account 2', - }); - - await driver.findElement('[data-testid="app-header-copy-button"]'); - }, - ); - }); - - it('should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding @no-mmi', async function () { - const testPrivateKey = - '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'; - - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await logInWithBalanceValidation(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'); - // 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 - await locateAccountBalanceDOM(driver); - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: '2nd account', - }); - - await driver.clickElement('[data-testid="account-menu-icon"]'); - const menuItems = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal(menuItems.length, 2); - - // User cannot delete 2nd account generated from the SRP imported in onboarding - await driver.clickElement( - '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', - ); - await driver.assertElementNotPresent( - '[data-testid="account-list-menu-remove"]', - ); - - // Create 3rd account with private key - await driver.clickElement('.mm-text-field'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement({ text: 'Import account', tag: 'button' }); - await driver.fill('#private-key-box', testPrivateKey); - - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', - ); - - // Wait for 3rd account to be created - await locateAccountBalanceDOM(driver); - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: 'Account 3', - }); - - // User can delete 3rd account imported with a private key - await driver.clickElement('[data-testid="account-menu-icon"]'); - const importedMenuItems = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal(importedMenuItems.length, 3); - await driver.clickElement( - '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', - ); - await driver.findElement('[data-testid="account-list-menu-remove"]'); - }, - ); - }); -}); diff --git a/test/e2e/tests/account/add-account.spec.ts b/test/e2e/tests/account/add-account.spec.ts new file mode 100644 index 000000000000..3fa65b14e87d --- /dev/null +++ b/test/e2e/tests/account/add-account.spec.ts @@ -0,0 +1,123 @@ +import { + withFixtures, + WALLET_PASSWORD, + defaultGanacheOptions, +} from '../../helpers'; +import { E2E_SRP } from '../../default-fixture'; +import FixtureBuilder from '../../fixture-builder'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import LoginPage from '../../page-objects/pages/login-page'; +import ResetPasswordPage from '../../page-objects/pages/reset-password-page'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { completeImportSRPOnboardingFlow } from '../../page-objects/flows/onboarding.flow'; +import { sendTransactionToAccount } from '../../page-objects/flows/send-transaction.flow'; + +describe('Add account', function () { + it('should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver, ganacheServer }) => { + await completeImportSRPOnboardingFlow({ driver }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.openAccountMenu(); + + // Create new account with default name Account 2 + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addNewAccountWithDefaultName(); + await headerNavbar.check_accountLabel('Account 2'); + await homePage.check_expectedBalanceIsDisplayed(); + + // Switch back to the first account and transfer some balance to 2nd account so they will not be removed after recovering SRP + await headerNavbar.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountDisplayedInAccountList('Account 1'); + await accountListPage.switchToAccount('Account 1'); + await headerNavbar.check_accountLabel('Account 1'); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + await sendTransactionToAccount({ + driver, + recipientAccount: 'Account 2', + amount: '2.8', + gasFee: '0.000042', + totalFee: '2.800042', + }); + await homePage.check_pageIsLoaded(); + await homePage.check_confirmedTxNumberDisplayedInActivity(); + await homePage.check_txAmountInActivity('-2.8 ETH'); + + // Lock wallet and recover via SRP in "forget password" option + await headerNavbar.lockMetaMask(); + await new LoginPage(driver).gotoResetPasswordPage(); + const resetPasswordPage = new ResetPasswordPage(driver); + await resetPasswordPage.check_pageIsLoaded(); + await resetPasswordPage.resetPassword(E2E_SRP, WALLET_PASSWORD); + + // Check wallet balance for both accounts + await homePage.check_pageIsLoaded(); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + await headerNavbar.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountDisplayedInAccountList('Account 2'); + await accountListPage.switchToAccount('Account 2'); + await headerNavbar.check_accountLabel('Account 2'); + await homePage.check_expectedBalanceIsDisplayed('2.8'); + }, + ); + }); + + it('should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding @no-mmi', async function () { + const testPrivateKey: string = + '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'; + + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithBalanceValidation(driver); + const headerNavbar = new HeaderNavbar(driver); + const homePage = new HomePage(driver); + await headerNavbar.openAccountMenu(); + + // Create new account with default name Account 2 + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addNewAccountWithDefaultName(); + await headerNavbar.check_accountLabel('Account 2'); + await homePage.check_expectedBalanceIsDisplayed(); + + // Check user cannot delete 2nd account generated from the SRP imported in onboarding + await headerNavbar.openAccountMenu(); + await accountListPage.check_removeAccountButtonIsNotDisplayed( + 'Account 1', + ); + + // Create 3rd account with private key + await accountListPage.addNewImportedAccount(testPrivateKey); + await headerNavbar.check_accountLabel('Account 3'); + await homePage.check_expectedBalanceIsDisplayed(); + + // Remove the 3rd account imported with a private key + await headerNavbar.openAccountMenu(); + await accountListPage.removeAccount('Account 3'); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await headerNavbar.openAccountMenu(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'Account 3', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/account/import-flow.spec.js b/test/e2e/tests/account/import-flow.spec.js deleted file mode 100644 index d2c84bfdc2b3..000000000000 --- a/test/e2e/tests/account/import-flow.spec.js +++ /dev/null @@ -1,474 +0,0 @@ -const { strict: assert } = require('assert'); -const path = require('path'); -const { - TEST_SEED_PHRASE, - convertToHexValue, - withFixtures, - regularDelayMs, - largeDelayMs, - completeImportSRPOnboardingFlow, - completeImportSRPOnboardingFlowWordByWord, - openActionMenuAndStartSendFlow, - unlockWallet, - logInWithBalanceValidation, - locateAccountBalanceDOM, - WALLET_PASSWORD, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); -const { emptyHtmlPage } = require('../../mock-e2e'); -const { isManifestV3 } = require('../../../../shared/modules/mv3.utils'); - -const ganacheOptions = { - accounts: [ - { - secretKey: - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', - balance: convertToHexValue(25000000000000000000), - }, - ], -}; - -async function mockTrezor(mockServer) { - return await mockServer - .forGet('https://connect.trezor.io/9/popup.html') - .thenCallback(() => { - return { - statusCode: 200, - body: emptyHtmlPage(), - }; - }); -} - -describe('Import flow @no-mmi', function () { - it('Import wallet using Secret Recovery Phrase', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - await driver.navigate(); - - await completeImportSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - // 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"]', - ); - await driver.clickElement('[data-testid="account-list-menu-details"]'); - await driver.findVisibleElement('.qr-code__wrapper'); - - // shows a QR code for the account - await driver.findVisibleElement( - '[data-testid="account-details-modal"]', - ); - // shows the correct account address - await driver.findElement('[data-testid="app-header-copy-button"]'); - - await driver.clickElement('button[aria-label="Close"]'); - await driver.assertElementNotPresent( - '[data-testid="account-details-modal"]', - ); - // logs out of the account - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement({ - css: '[data-testid="global-menu-lock"]', - text: 'Lock MetaMask', - }); - - // accepts the account password after lock - await unlockWallet(driver, { - navigate: false, - waitLoginSuccess: false, - }); - - // Create a new account - // switches to localhost - await driver.delay(largeDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - await driver.clickElement('.toggle-button'); - await driver.clickElement({ text: 'Localhost', tag: 'p' }); - - // 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"]', - ); - await driver.clickElement({ - text: 'Add a new Ethereum account', - tag: 'button', - }); - - // set account name - await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - - // should show the correct account name - const accountName = await driver.isElementPresent({ - tag: 'span', - text: '2nd account', - }); - - assert.equal(accountName, true, 'Account name is not correct'); - - // Switch back to original account - // chooses the original account from the account menu - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '.multichain-account-list-item__account-name__button', - ); - - // Send ETH from inside MetaMask - // starts a send transaction - await locateAccountBalanceDOM(driver, ganacheServer); - await openActionMenuAndStartSendFlow(driver); - await driver.fill( - 'input[placeholder="Enter public address (0x) or domain name"]', - '0x2f318C334780961FB129D2a6c30D0763d9a5C970', - ); - await driver.fill('input[placeholder="0"]', '1'); - // Continue to next screen - await driver.clickElement({ text: 'Continue', tag: 'button' }); - - // confirms the transaction - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - - // finds the transaction in the transactions list - await driver.clickElement( - '[data-testid="account-overview__activity-tab"]', - ); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .activity-list-item', - ); - return confirmedTxes.length === 1; - }, 10000); - - const txValues = await driver.findElements( - '[data-testid="transaction-list-item-primary-currency"]', - ); - assert.equal(txValues.length, 1); - assert.ok(/-1\s*ETH/u.test(await txValues[0].getText())); - }, - ); - }); - - it('Import wallet using Secret Recovery Phrase with pasting word by word', async function () { - const testAddress = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3'; - - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await completeImportSRPOnboardingFlowWordByWord( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - // 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"]', - ); - await driver.clickElement('[data-testid="account-list-menu-details"'); - await driver.findVisibleElement('.qr-code__wrapper'); - - // Extract address segments from the DOM - const outerSegment = await driver.findElement( - '.qr-code__address-segments', - ); - - // Get the text content of each segment - const displayedAddress = await outerSegment.getText(); - - // Assert that the displayed address matches the testAddress - assert.strictEqual( - displayedAddress.toLowerCase(), - testAddress.toLowerCase(), - 'The displayed address does not match the test address', - ); - }, - ); - }); - - it('Import Account using private key and remove imported account', async function () { - const testPrivateKey1 = - '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'; - const testPrivateKey2 = - 'F4EC2590A0C10DE95FBF4547845178910E40F5035320C516A18C117DE02B5669'; - - await withFixtures( - { - fixtures: new FixtureBuilder() - .withKeyringControllerImportedAccountVault() - .withPreferencesControllerImportedAccountIdentities() - .withAccountsControllerImportedAccount() - .build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - 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"]', - ); - await driver.clickElement({ text: 'Import account', tag: 'button' }); - - // Imports Account 4 with private key - await driver.findClickableElement('#private-key-box'); - await driver.fill('#private-key-box', testPrivateKey1); - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', - ); - - // New imported account has correct name and label - await driver.findClickableElement({ - css: '[data-testid="account-menu-icon"]', - text: 'Account 4', - }); - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.findElement({ - css: `.multichain-account-list-item--selected .multichain-account-list-item__content .mm-tag`, - 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"]', - ); - await driver.clickElement({ text: 'Import account', tag: 'button' }); - await driver.findClickableElement('#private-key-box'); - await driver.fill('#private-key-box', testPrivateKey2); - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', - ); - - // New imported account has correct name and label - await driver.findClickableElement({ - css: '[data-testid="account-menu-icon"]', - text: 'Account 5', - }); - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountListItems = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal(accountListItems.length, 5); - - await driver.clickElement( - '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', - ); - - // Account 5 can be removed - await driver.clickElement('[data-testid="account-list-menu-remove"]'); - await driver.clickElement({ text: 'Remove', tag: 'button' }); - - await driver.delay(1000); - await driver.clickElementUsingMouseMove({ - css: '[data-testid="account-menu-icon"]', - text: 'Account 4', - }); - const accountListItemsAfterRemoval = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal(accountListItemsAfterRemoval.length, 4); - }, - ); - }); - - it('Import Account using json file', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder() - .withKeyringControllerImportedAccountVault() - .withPreferencesControllerImportedAccountIdentities() - .withAccountsControllerImportedAccount() - .build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - 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"]', - ); - - await driver.clickElement({ text: 'Import account', tag: 'button' }); - - await driver.clickElement('.dropdown__select'); - await driver.clickElement({ text: 'JSON File', tag: 'option' }); - - const fileInput = await driver.findElement('input[type="file"]'); - const importJsonFile = path.join( - __dirname, - '../..', - 'import-utc-json', - 'test-json-import-account-file.json', - ); - - fileInput.sendKeys(importJsonFile); - - await driver.fill('#json-password-box', 'foobarbazqux'); - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', - ); - - const importedAccount = '0x0961Ca10D49B9B8e371aA0Bcf77fE5730b18f2E4'; - await locateAccountBalanceDOM(driver, ganacheServer, importedAccount); - // New imported account has correct name and label - await driver.findClickableElement({ - css: '[data-testid="account-menu-icon"]', - text: 'Account 4', - }); - - await driver.clickElement('[data-testid="account-menu-icon"]'); - - await driver.findElement({ - css: `.multichain-account-list-item--selected .multichain-account-list-item__content .mm-tag`, - text: 'Imported', - }); - - const accountListItems = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal(accountListItems.length, 4); - }, - ); - }); - - it('Import Account using private key of an already active account should result in an error', async function () { - const testPrivateKey = - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9'; - await withFixtures( - { - fixtures: new FixtureBuilder() - .withKeyringControllerImportedAccountVault() - .withPreferencesControllerImportedAccountIdentities() - .withAccountsControllerImportedAccount() - .build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // 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"]', - ); - await driver.clickElement({ text: 'Import account', tag: 'button' }); - - // enter private key - await driver.findClickableElement('#private-key-box'); - await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', - ); - - // error should occur - await driver.waitForSelector({ - css: '.mm-help-text', - text: 'The account you are trying to import is a duplicate', - }); - }, - ); - }); - - it('Connects to a Hardware wallet for lattice', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockTrezor, - }, - async ({ driver }) => { - await unlockWallet(driver); - - // 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"]', - ); - await driver.clickElement({ - text: 'Add hardware wallet', - tag: 'button', - }); - await driver.findClickableElement( - '[data-testid="hardware-connect-close-btn"]', - ); - await driver.clickElement('[data-testid="connect-lattice-btn"]'); - await driver.findClickableElement({ - text: 'Continue', - tag: 'button', - }); - - await driver.clickElement({ text: 'Continue', tag: 'button' }); - - const allWindows = await driver.waitUntilXWindowHandles(2); - - assert.equal(allWindows.length, isManifestV3 ? 3 : 2); - }, - ); - }); -}); diff --git a/test/e2e/tests/account/import-flow.spec.ts b/test/e2e/tests/account/import-flow.spec.ts new file mode 100644 index 000000000000..8dc989db4d41 --- /dev/null +++ b/test/e2e/tests/account/import-flow.spec.ts @@ -0,0 +1,115 @@ +import path from 'path'; +import { withFixtures } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { completeImportSRPOnboardingFlow } from '../../page-objects/flows/onboarding.flow'; + +describe('Import flow @no-mmi', function () { + it('Import wallet using Secret Recovery Phrase with pasting word by word', async function () { + const testAddress = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + fillSrpWordByWord: true, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // Open account details modal and check displayed account address + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('Account 1'); + await headerNavbar.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.openAccountDetailsModal('Account 1'); + await accountListPage.check_addressInAccountDetailsModal( + testAddress.toLowerCase(), + ); + }, + ); + }); + + it('Import Account using json file', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withKeyringControllerImportedAccountVault() + .withPreferencesControllerImportedAccountIdentities() + .withAccountsControllerImportedAccount() + .build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithBalanceValidation(driver); + + // Wait until account list is loaded to mitigate race condition + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('Account 1'); + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + + // Imports an account with JSON file + const jsonFile = path.join( + __dirname, + '../..', + 'import-utc-json', + 'test-json-import-account-file.json', + ); + await accountListPage.importAccountWithJsonFile( + jsonFile, + 'foobarbazqux', + ); + + // Check new imported account has correct name and label + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await headerNavbar.check_accountLabel('Account 4'); + + await headerNavbar.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts(4); + await accountListPage.check_currentAccountIsImported(); + }, + ); + }); + + it('Import Account using private key of an already active account should result in an error', async function () { + const testPrivateKey = + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9'; + await withFixtures( + { + fixtures: new FixtureBuilder() + .withKeyringControllerImportedAccountVault() + .build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithBalanceValidation(driver); + + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('Account 1'); + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + + // import active account with private key from the account menu and check error message + await accountListPage.addNewImportedAccount( + testPrivateKey, + 'The account you are trying to import is a duplicate', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts index fd2fe013c3c1..0c528b1dc20c 100644 --- a/test/e2e/tests/account/snap-account-signatures.spec.ts +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -18,7 +18,7 @@ import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring- 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 + this.timeout(200000); // 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) diff --git a/test/e2e/tests/account/snap-account-transfers.spec.ts b/test/e2e/tests/account/snap-account-transfers.spec.ts index 23cc5d510eb2..ee2466cf28e1 100644 --- a/test/e2e/tests/account/snap-account-transfers.spec.ts +++ b/test/e2e/tests/account/snap-account-transfers.spec.ts @@ -47,13 +47,13 @@ describe('Snap Account Transfers @no-mmi', function (this: Suite) { await headerNavbar.check_accountLabel('SSK Account'); // send 1 ETH from snap account to account 1 - await sendTransactionWithSnapAccount( + await sendTransactionWithSnapAccount({ driver, - DEFAULT_FIXTURE_ACCOUNT, - '1', - '0.000042', - '1.000042', - ); + recipientAddress: DEFAULT_FIXTURE_ACCOUNT, + amount: '1', + gasFee: '0.000042', + totalFee: '1.000042', + }); await headerNavbar.check_pageIsLoaded(); await headerNavbar.openAccountMenu(); const accountList = new AccountListPage(driver); @@ -95,14 +95,14 @@ describe('Snap Account Transfers @no-mmi', function (this: Suite) { await headerNavbar.check_accountLabel('SSK Account'); // send 1 ETH from snap account to account 1 and approve the transaction - await sendTransactionWithSnapAccount( + await sendTransactionWithSnapAccount({ driver, - DEFAULT_FIXTURE_ACCOUNT, - '1', - '0.000042', - '1.000042', - false, - ); + recipientAddress: DEFAULT_FIXTURE_ACCOUNT, + amount: '1', + gasFee: '0.000042', + totalFee: '1.000042', + isSyncFlow: false, + }); await headerNavbar.check_pageIsLoaded(); await headerNavbar.openAccountMenu(); const accountList = new AccountListPage(driver); @@ -145,15 +145,15 @@ describe('Snap Account Transfers @no-mmi', function (this: Suite) { await headerNavbar.check_accountLabel('SSK Account'); // send 1 ETH from snap account to account 1 and reject the transaction - await sendTransactionWithSnapAccount( + await sendTransactionWithSnapAccount({ driver, - DEFAULT_FIXTURE_ACCOUNT, - '1', - '0.000042', - '1.000042', - false, - false, - ); + recipientAddress: DEFAULT_FIXTURE_ACCOUNT, + amount: '1', + gasFee: '0.000042', + totalFee: '1.000042', + isSyncFlow: false, + approveTransaction: false, + }); // check the transaction is failed in MetaMask activity list const homepage = new HomePage(driver); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 1f4a3e5cda79..930f0196673e 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -11,8 +11,8 @@ import { import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { Driver } from '../../webdriver/driver'; -import { FeatureFlagResponse } from '../../../../ui/pages/bridge/bridge.util'; import { isManifestV3 } from '../../../../shared/modules/mv3.utils'; +import { FeatureFlagResponse } from '../../../../ui/pages/bridge/types'; import { DEFAULT_FEATURE_FLAGS_RESPONSE, ETH_CONVERSION_RATE_USD, diff --git a/test/e2e/tests/bridge/constants.ts b/test/e2e/tests/bridge/constants.ts index e79a1dfe4553..ae7fc37a62c6 100644 --- a/test/e2e/tests/bridge/constants.ts +++ b/test/e2e/tests/bridge/constants.ts @@ -1,6 +1,10 @@ -import { FeatureFlagResponse } from '../../../../ui/pages/bridge/bridge.util'; +import { FeatureFlagResponse } from '../../../../ui/pages/bridge/types'; export const DEFAULT_FEATURE_FLAGS_RESPONSE: FeatureFlagResponse = { + 'extension-config': { + refreshRate: 30, + maxRefreshCount: 5, + }, 'extension-support': false, 'src-network-allowlist': [1, 42161, 59144], 'dest-network-allowlist': [1, 42161, 59144], diff --git a/test/e2e/tests/confirmations/helpers.ts b/test/e2e/tests/confirmations/helpers.ts index ff467f42c320..355f664ec61c 100644 --- a/test/e2e/tests/confirmations/helpers.ts +++ b/test/e2e/tests/confirmations/helpers.ts @@ -46,8 +46,8 @@ export function withRedesignConfirmationFixtures( transactionEnvelopeType === TransactionEnvelopeType.legacy ? defaultGanacheOptions : defaultGanacheOptionsForType2Transactions, - smartContract, - testSpecificMock: mocks, + ...(smartContract && { smartContract }), + ...(mocks && { testSpecificMock: mocks }), title, }, testFunction, diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts index 747ba15872b3..38d29ad3ad77 100644 --- a/test/e2e/tests/confirmations/navigation.spec.ts +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -66,10 +66,15 @@ describe('Navigation Signature - Different signature types', function (this: Sui '[data-testid="confirm-nav__next-confirmation"]', ); - // Verify Transaction Sending ETH is displayed - await verifyTransaction(driver, 'Sending ETH'); + // Verify simple send transaction is displayed + await driver.waitForSelector({ + tag: 'h3', + text: 'Transfer request', + }); - await driver.clickElement('[data-testid="next-page"]'); + await driver.clickElement( + '[data-testid="confirm-nav__next-confirmation"]', + ); // Verify Sign Typed Data v3 confirmation is displayed await verifySignedTypeV3Confirmation(driver); @@ -78,10 +83,15 @@ describe('Navigation Signature - Different signature types', function (this: Sui '[data-testid="confirm-nav__previous-confirmation"]', ); - // Verify Sign Typed Data v3 confirmation is displayed - await verifyTransaction(driver, 'Sending ETH'); + // Verify simple send transaction is displayed + await driver.waitForSelector({ + tag: 'h3', + text: 'Transfer request', + }); - await driver.clickElement('[data-testid="previous-page"]'); + await driver.clickElement( + '[data-testid="confirm-nav__previous-confirmation"]', + ); // Verify Sign Typed Data v3 confirmation is displayed await verifySignTypedData(driver); @@ -179,13 +189,3 @@ async function queueSignaturesAndTransactions(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } - -async function verifyTransaction( - driver: Driver, - expectedTransactionType: string, -) { - await driver.waitForSelector({ - tag: 'span', - text: expectedTransactionType, - }); -} diff --git a/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts new file mode 100644 index 000000000000..383a3bd6b924 --- /dev/null +++ b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts @@ -0,0 +1,187 @@ +import { strict as assert } from 'assert'; +import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { Suite } from 'mocha'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; +import { Ganache } from '../../../seeder/ganache'; +import { Driver } from '../../../webdriver/driver'; +import { + mockSignatureApproved, + mockSignatureRejected, + scrollAndConfirmAndAssertConfirm, + withRedesignConfirmationFixtures, +} from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertAccountDetailsMetrics, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerDeploy, + SignatureType, + triggerSignature, +} from './signature-helpers'; + +describe('Confirmation Signature - NFT Permit @no-mmi', function (this: Suite) { + it('initiates and confirms and emits the correct events', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ + driver, + ganacheServer, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); + const publicAddress = addresses?.[0] as string; + + await openDappAndTriggerDeploy(driver); + await driver.delay(1000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement('[data-testid="confirm-footer-button"]'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.delay(1000); + await triggerSignature(driver, SignatureType.NFTPermit); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await clickHeaderInfoBtn(driver); + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await assertInfoValues(driver); + await scrollAndConfirmAndAssertConfirm(driver); + await driver.delay(1000); + + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v4', + ); + + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Permit', + uiCustomizations: ['redesigned_confirmation', 'permit'], + }); + + await assertVerifiedResults(driver, publicAddress); + }, + mockSignatureApproved, + ); + }); + + it('initiates and rejects and emits the correct events', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerDeploy(driver); + await driver.delay(1000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement('[data-testid="confirm-footer-button"]'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.delay(1000); + await triggerSignature(driver, SignatureType.NFTPermit); + + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-footer-cancel-button"]', + ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await driver.waitForSelector({ + tag: 'span', + text: 'Error: User rejected the request.', + }); + + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Permit', + uiCustomizations: ['redesigned_confirmation', 'permit'], + location: 'confirmation', + }); + }, + mockSignatureRejected, + ); + }); +}); + +async function assertInfoValues(driver: Driver) { + await driver.clickElement('[data-testid="sectionCollapseButton"]'); + const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); + const contractPetName = driver.findElement({ + css: '.name__value', + text: '0x581c3...45947', + }); + + const title = driver.findElement({ text: 'Withdrawal request' }); + const description = driver.findElement({ + text: 'This site wants permission to withdraw your NFTs', + }); + const primaryType = driver.findElement({ text: 'Permit' }); + const spender = driver.findElement({ + css: '.name__value', + text: '0x581c3...45947', + }); + const tokenId = driver.findElement({ text: '3606393' }); + const nonce = driver.findElement({ text: '0' }); + const deadline = driver.findElement({ text: '23 December 2024, 23:03' }); + + assert.ok(await origin, 'origin'); + assert.ok(await contractPetName, 'contractPetName'); + assert.ok(await title, 'title'); + assert.ok(await description, 'description'); + assert.ok(await primaryType, 'primaryType'); + assert.ok(await spender, 'spender'); + assert.ok(await tokenId, 'tokenId'); + assert.ok(await nonce, 'nonce'); + assert.ok(await deadline, 'deadline'); +} + +async function assertVerifiedResults(driver: Driver, publicAddress: string) { + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#sign721PermitVerify'); + + await driver.waitForSelector({ + css: '#sign721PermitVerifyResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#sign721PermitResult', + text: '0x572bc6300f6aa669e85e0a7792bc0b0803fb70c3c492226b30007ff7030b03600e390ef295a5a525d19f444943ae82697f0e5b5b0d77cc382cb2ea9486ec27801c', + }); + + await driver.waitForSelector({ + css: '#sign721PermitResultR', + text: 'r: 0x572bc6300f6aa669e85e0a7792bc0b0803fb70c3c492226b30007ff7030b0360', + }); + + await driver.waitForSelector({ + css: '#sign721PermitResultS', + text: 's: 0x0e390ef295a5a525d19f444943ae82697f0e5b5b0d77cc382cb2ea9486ec2780', + }); + + await driver.waitForSelector({ + css: '#sign721PermitResultV', + text: 'v: 28', + }); + + await driver.waitForSelector({ + css: '#sign721PermitVerifyResult', + text: publicAddress, + }); +} diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 8da5e411a2f4..bc74b9fd2f5f 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -114,6 +114,7 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { }); async function assertInfoValues(driver: Driver) { + await driver.clickElement('[data-testid="sectionCollapseButton"]'); const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); const contractPetName = driver.findElement({ css: '.name__value', diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index 9b87e5b4e9cc..5242be3f3c20 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -14,6 +14,7 @@ export const WALLET_ETH_BALANCE = '25'; export enum SignatureType { PersonalSign = '#personalSign', Permit = '#signPermit', + NFTPermit = '#sign721Permit', SignTypedDataV3 = '#signTypedDataV3', SignTypedDataV4 = '#signTypedDataV4', SignTypedData = '#signTypedData', @@ -240,12 +241,23 @@ export async function assertPastedAddress(driver: Driver) { assert.equal(await formFieldEl.getAttribute('value'), WALLET_ADDRESS); } +export async function triggerSignature(driver: Driver, type: string) { + await driver.clickElement(type); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); +} + export async function openDappAndTriggerSignature( driver: Driver, type: string, ) { await unlockWallet(driver); await openDapp(driver); - await driver.clickElement(type); + await triggerSignature(driver, type); +} + +export async function openDappAndTriggerDeploy(driver: Driver) { + await unlockWallet(driver); + await openDapp(driver); + await driver.clickElement('#deployNFTsButton'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); } diff --git a/test/e2e/tests/confirmations/signatures/siwe.spec.ts b/test/e2e/tests/confirmations/signatures/siwe.spec.ts index 1dd545034731..2a7b43ba1099 100644 --- a/test/e2e/tests/confirmations/signatures/siwe.spec.ts +++ b/test/e2e/tests/confirmations/signatures/siwe.spec.ts @@ -103,6 +103,7 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { }); async function assertInfoValues(driver: Driver) { + await driver.clickElement('[data-testid="sectionCollapseButton"]'); const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); const message = driver.findElement({ text: 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', 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 index 7bacf156b71a..9cd1d3837729 100644 --- a/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc20-token-send-redesign.spec.ts @@ -144,8 +144,8 @@ async function createWalletInitiatedTransactionAndAssertDetails( await sendToPage.fillRecipient('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); await sendToPage.fillAmount('1'); - await sendToPage.click_assetPickerButton(); - await sendToPage.click_secondTokenListButton(); + await sendToPage.clickAssetPickerButton(); + await sendToPage.clickSecondTokenListButton(); await sendToPage.goToNextScreen(); const tokenTransferTransactionConfirmation = diff --git a/test/e2e/tests/confirmations/transactions/native-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/native-send-redesign.spec.ts new file mode 100644 index 000000000000..e8226977d019 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/native-send-redesign.spec.ts @@ -0,0 +1,113 @@ +/* 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 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 { Driver } from '../../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from '../helpers'; +import { TestSuiteArguments } from './shared'; + +const TOKEN_RECIPIENT_ADDRESS = '0x2f318C334780961FB129D2a6c30D0763d9a5C970'; + +describe('Confirmation Redesign Native 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 }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + }); + + describe('dApp initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + }); +}); + +async function createWalletInitiatedTransactionAndAssertDetails( + driver: Driver, +) { + await unlockWallet(driver); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress: null, url: DAPP_URL }); + + 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(TOKEN_RECIPIENT_ADDRESS); + await sendToPage.fillAmount('1'); + await sendToPage.goToNextScreen(); + + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_walletInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} + +async function createDAppInitiatedTransactionAndAssertDetails(driver: Driver) { + await unlockWallet(driver); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress: null, url: DAPP_URL }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await testDapp.clickSimpleSendButton(); + + 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_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickScrollToBottomButton(); + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} diff --git a/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts new file mode 100644 index 000000000000..4f6093e349f2 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts @@ -0,0 +1,320 @@ +/* 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 TransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/transaction-confirmation'; +import HomePage from '../../../page-objects/pages/homepage'; +import NFTDetailsPage from '../../../page-objects/pages/nft-details-page'; +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'); + +const TOKEN_RECIPIENT_ADDRESS = '0x2f318C334780961FB129D2a6c30D0763d9a5C970'; + +describe('Confirmation Redesign Token Send @no-mmi', function () { + describe('ERC721', 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 createERC721WalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + erc721Mocks, + SMART_CONTRACTS.NFTS, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createERC721WalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + erc721Mocks, + SMART_CONTRACTS.NFTS, + ); + }); + }); + + 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 createERC721DAppInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + erc721Mocks, + SMART_CONTRACTS.NFTS, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createERC721DAppInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + erc721Mocks, + SMART_CONTRACTS.NFTS, + ); + }); + }); + }); + + describe('ERC1155', 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 createERC1155WalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + erc1155Mocks, + SMART_CONTRACTS.ERC1155, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await createERC1155WalletInitiatedTransactionAndAssertDetails( + driver, + contractRegistry, + ); + }, + erc1155Mocks, + SMART_CONTRACTS.ERC1155, + ); + }); + }); + }); +}); + +async function erc721Mocks(server: Mockttp) { + return [await mockedERC7214BytesNFTTokenSend(server)]; +} + +async function erc1155Mocks(server: Mockttp) { + return [await mockedERC11554BytesNFTTokenSend(server)]; +} + +export async function mockedERC7214BytesNFTTokenSend(mockServer: Mockttp) { + return await mockServer + .forGet('https://www.4byte.directory/api/v1/signatures/') + .withQuery({ hex_signature: '0x23b872dd' }) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + count: 1, + next: null, + previous: null, + results: [ + { + bytes_signature: '#rÝ', + created_at: '2016-07-09T03:58:28.927638Z', + hex_signature: '0x23b872dd', + id: 147, + text_signature: 'transferFrom(address,address,uint256)', + }, + ], + }, + })); +} + +export async function mockedERC11554BytesNFTTokenSend(mockServer: Mockttp) { + return await mockServer + .forGet('https://www.4byte.directory/api/v1/signatures/') + .withQuery({ hex_signature: '0xf242432a' }) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + count: 1, + next: null, + previous: null, + results: [ + { + bytes_signature: 'òBC*', + created_at: '2018-08-29T20:16:41.650553Z', + hex_signature: '0xf242432a', + id: 93843, + text_signature: + 'safeTransferFrom(address,address,uint256,uint256,bytes)', + }, + ], + }, + })); +} + +async function createERC721WalletInitiatedTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.NFTS); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + + await testDapp.clickERC721MintButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + const mintConfirmation = new TransactionConfirmation(driver); + + await mintConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + const homePage = new HomePage(driver); + await homePage.goToNFTList(); + await homePage.clickNFTIconOnActivityList(); + + const nftDetailsPage = new NFTDetailsPage(driver); + await nftDetailsPage.clickNFTSendButton(); + + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.fillRecipient(TOKEN_RECIPIENT_ADDRESS); + 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.clickScrollToBottomButton(); + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} + +async function createERC721DAppInitiatedTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.NFTS); + + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + await testDapp.clickERC721MintButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const mintConfirmation = new TransactionConfirmation(driver); + await mintConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await testDapp.clickERC721TransferFromButton(); + + 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.clickScrollToBottomButton(); + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} + +async function createERC1155WalletInitiatedTransactionAndAssertDetails( + driver: Driver, + contractRegistry?: GanacheContractAddressRegistry, +) { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(SMART_CONTRACTS.ERC1155); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + await testDapp.fillERC1155TokenID('1'); + await testDapp.fillERC1155TokenAmount('1'); + await testDapp.clickERC1155MintButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const mintConfirmation = new TransactionConfirmation(driver); + await mintConfirmation.clickFooterConfirmButton(); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await testDapp.clickERC1155WatchButton(); + + await driver.delay(veryLargeDelayMs); + 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.goToNFTList(); + await homePage.clickNFTIconOnActivityList(); + + const nftDetailsPage = new NFTDetailsPage(driver); + await nftDetailsPage.clickNFTSendButton(); + + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.fillRecipient(TOKEN_RECIPIENT_ADDRESS); + await sendToPage.fillNFTAmount('1'); + 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.clickScrollToBottomButton(); + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} diff --git a/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html new file mode 100644 index 000000000000..dae42d094890 --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html @@ -0,0 +1,9 @@ + + + + Mock CSP header testing + + +
Mock Page for Content-Security-Policy header testing
+ + diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts new file mode 100644 index 000000000000..996c64343c5b --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + openDapp, + unlockWallet, + withFixtures, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; + +describe('Content-Security-Policy', function (this: Suite) { + it('opening a restricted website should still load the extension', async function () { + await withFixtures( + { + dapp: true, + dappPaths: [ + './tests/content-security-policy/content-security-policy-mock-page', + ], + staticServerOptions: { + headers: [ + { + source: 'index.html', + headers: [ + { + key: 'Content-Security-Policy', + value: `default-src 'none'`, + }, + ], + }, + ], + }, + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await openDapp(driver); + const isExtensionLoaded: boolean = await driver.executeScript( + 'return typeof window.ethereum !== "undefined"', + ); + assert.equal(isExtensionLoaded, true); + }, + ); + }); +}); diff --git a/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js b/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js index b992925ffc7a..584408134f1a 100644 --- a/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js +++ b/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js @@ -31,7 +31,7 @@ describe('Dapp interactions', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await unlockWallet(driver); const notification = await driver.isElementPresent({ - text: 'Allow this site to add a network?', + text: 'Add Localhost 8546', tag: 'h3', }); diff --git a/test/e2e/tests/hardware-wallets/lattice-connect.spec.ts b/test/e2e/tests/hardware-wallets/lattice-connect.spec.ts new file mode 100644 index 000000000000..b8497a9df692 --- /dev/null +++ b/test/e2e/tests/hardware-wallets/lattice-connect.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { withFixtures, unlockWallet } from '../../helpers'; +import { isManifestV3 } from '../../../../shared/modules/mv3.utils'; + +describe('Lattice hardware wallet @no-mmi', function (this: Suite) { + it('connects to lattice hardware wallet', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + // 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"]', + ); + await driver.clickElement({ + text: 'Add hardware wallet', + tag: 'button', + }); + await driver.findClickableElement( + '[data-testid="hardware-connect-close-btn"]', + ); + await driver.clickElement('[data-testid="connect-lattice-btn"]'); + await driver.clickElement({ text: 'Continue', tag: 'button' }); + + const allWindows = await driver.waitUntilXWindowHandles(2); + assert.equal(allWindows.length, isManifestV3 ? 3 : 2); + }, + ); + }); +}); diff --git a/test/e2e/tests/hardware-wallets/trezor-sign.spec.ts b/test/e2e/tests/hardware-wallets/trezor-sign.spec.ts index 169897ed7b16..f4cbf87b9dd4 100644 --- a/test/e2e/tests/hardware-wallets/trezor-sign.spec.ts +++ b/test/e2e/tests/hardware-wallets/trezor-sign.spec.ts @@ -31,9 +31,11 @@ describe('Trezor Hardware Signatures', function (this: Suite) { await openDapp(driver); await driver.clickElement('#signTypedDataV4'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.delay(1000); - await driver.clickElement('.confirm-scroll-to-bottom__button'); + await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.delay(1000); await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); diff --git a/test/e2e/tests/metrics/developer-options-sentry.spec.ts b/test/e2e/tests/metrics/developer-options-sentry.spec.ts new file mode 100644 index 000000000000..3c20f931f302 --- /dev/null +++ b/test/e2e/tests/metrics/developer-options-sentry.spec.ts @@ -0,0 +1,88 @@ +import { Suite } from 'mocha'; +import { MockttpServer } from 'mockttp'; +import { withFixtures, sentryRegEx } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import DevelopOptions from '../../page-objects/pages/developer-options-page'; +import ErrorPage from '../../page-objects/pages/error-page'; + +const triggerCrash = async (driver: Driver): Promise => { + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptions(driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); +}; + +async function mockSentryError(mockServer: MockttpServer) { + return [ + await mockServer + .forPost(sentryRegEx) + .withBodyIncluding('feedback') + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }), + ]; +} + +describe('Developer Options - Sentry', function (this: Suite) { + it('gives option to cause a page crash and provides sentry form to report', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + title: this.test?.fullTitle(), + testSpecificMock: mockSentryError, + ignoredConsoleErrors: [ + 'Error#1: Unable to find value of key "developerOptions" for locale "en"', + 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.', + ], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + await errorPage.validate_errorMessage(); + await errorPage.submitToSentryUserFeedbackForm(); + await errorPage.waitForSentrySuccessModal(); + }, + ); + }); + + it('gives option to cause a page crash and offer contact support option', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + ignoredConsoleErrors: [ + 'Error#1: Unable to find value of key "developerOptions" for locale "en"', + 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.', + ], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + + await errorPage.contactAndValidateMetaMaskSupport(); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index dfe77f758fcb..d4c27a87aba6 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -13,6 +13,7 @@ const { convertToHexValue, logInWithBalanceValidation, withFixtures, + sentryRegEx, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -46,6 +47,8 @@ const maskedBackgroundFields = [ 'AppStateController.notificationGasPollTokens', 'AppStateController.popupGasPollTokens', 'CurrencyController.currencyRates.ETH.conversionDate', + 'CurrencyController.currencyRates.LineaETH.conversionDate', + 'CurrencyController.currencyRates.SepoliaETH.conversionDate', ]; const maskedUiFields = maskedBackgroundFields.map(backgroundToUiField); @@ -57,6 +60,7 @@ const removedBackgroundFields = [ 'AppStateController.currentPopupId', 'AppStateController.timeoutMinutes', 'AppStateController.lastInteractedConfirmationInfo', + 'BridgeController.bridgeState.quoteRequest.walletAddress', 'PPOMController.chainStatus.0x539.lastVisited', 'PPOMController.versionInfo', // This property is timing-dependent @@ -178,8 +182,6 @@ function getMissingProperties(complete, object) { } describe('Sentry errors', function () { - const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu; - const migrationError = process.env.SELENIUM_BROWSER === Browser.CHROME ? `"type":"TypeError","value":"Cannot read properties of undefined (reading 'version')` @@ -862,6 +864,19 @@ describe('Sentry errors', function () { it('should not have extra properties in UI state mask @no-mmi', async function () { const expectedMissingState = { + bridgeState: { + // This can get wiped out during initialization due to a bug in + // the "resetState" method + quoteRequest: { + destChainId: true, + destTokenAddress: true, + srcChainId: true, + srcTokenAmount: true, + walletAddress: false, + }, + quotesLastFetched: true, + quotesLoadingStatus: true, + }, currentPopupId: false, // Initialized as undefined // Part of transaction controller store, but missing from the initial // state @@ -869,6 +884,7 @@ describe('Sentry errors', function () { preferences: { autoLockTimeLimit: true, // Initialized as undefined showConfirmationAdvancedDetails: true, + privacyMode: false, }, smartTransactionsState: { fees: { diff --git a/test/e2e/tests/metrics/nft-detection-metrics.spec.js b/test/e2e/tests/metrics/nft-detection-metrics.spec.js index 3c77fdb66731..a0c901087425 100644 --- a/test/e2e/tests/metrics/nft-detection-metrics.spec.js +++ b/test/e2e/tests/metrics/nft-detection-metrics.spec.js @@ -101,7 +101,7 @@ describe('Nft detection event @no-mmi', function () { locale: 'en', chain_id: '0x539', environment_type: 'fullscreen', - is_profile_syncing_enabled: null, + is_profile_syncing_enabled: true, }); assert.deepStrictEqual(events[2].properties, { nft_autodetection_enabled: true, 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 f96d03d96da0..cebacab2b1d9 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 @@ -62,12 +62,18 @@ "BridgeController": { "bridgeState": { "bridgeFeatureFlags": { + "extensionConfig": "object", "extensionSupport": "boolean", "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, "destTokens": {}, "destTopAssets": {}, + "quoteRequest": { + "slippage": 0.5, + "srcTokenAddress": "0x0000000000000000000000000000000000000000" + }, + "quotes": {}, "srcTokens": {}, "srcTopAssets": {} } @@ -79,6 +85,16 @@ "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 + }, + "LineaETH": { + "conversionDate": "number", + "conversionRate": 1700, + "usdConversionRate": 1700 + }, + "SepoliaETH": { + "conversionDate": "number", + "conversionRate": 1700, + "usdConversionRate": 1700 } }, "currentCurrency": "usd" @@ -128,7 +144,7 @@ "MultichainBalancesController": { "balances": "object" }, "MultichainRatesController": { "fiatCurrency": "usd", - "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, + "rates": { "btc": { "conversionDate": 0, "conversionRate": 0 } }, "cryptocurrencies": ["btc"] }, "NameController": { "names": "object", "nameSources": "object" }, @@ -182,6 +198,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": "boolean", "useTokenDetection": true, @@ -208,7 +225,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showMultiRpcModal": "boolean", 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 c4c4ea71609d..97399d34c508 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 @@ -30,7 +30,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showMultiRpcModal": "boolean", @@ -54,6 +54,16 @@ "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 + }, + "LineaETH": { + "conversionDate": "number", + "conversionRate": 1700, + "usdConversionRate": 1700 + }, + "SepoliaETH": { + "conversionDate": "number", + "conversionRate": 1700, + "usdConversionRate": 1700 } }, "connectedStatusPopoverHasBeenShown": true, @@ -105,6 +115,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": true, "useTokenDetection": true, @@ -187,7 +198,7 @@ "lastFetchedBlockNumbers": "object", "submitHistory": "object", "fiatCurrency": "usd", - "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, + "rates": { "btc": { "conversionDate": 0, "conversionRate": 0 } }, "cryptocurrencies": ["btc"], "snaps": "object", "jobs": "object", @@ -203,7 +214,6 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", - "submitHistory": "object", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", @@ -258,12 +268,18 @@ }, "bridgeState": { "bridgeFeatureFlags": { + "extensionConfig": "object", "extensionSupport": "boolean", "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, "destTokens": {}, "destTopAssets": {}, + "quoteRequest": { + "slippage": 0.5, + "srcTokenAddress": "0x0000000000000000000000000000000000000000" + }, + "quotes": {}, "srcTokens": {}, "srcTopAssets": {} }, 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 89b1b29100bb..7622ad15937c 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 @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", @@ -128,7 +129,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "isRedesignedConfirmationsDeveloperEnabled": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index f13d3e078c64..b3fa8d117beb 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 @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", @@ -128,7 +129,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "isRedesignedConfirmationsDeveloperEnabled": "boolean", diff --git a/test/e2e/tests/metrics/token-detection-metrics.spec.js b/test/e2e/tests/metrics/token-detection-metrics.spec.js index 669aff0a9290..923f7c86a242 100644 --- a/test/e2e/tests/metrics/token-detection-metrics.spec.js +++ b/test/e2e/tests/metrics/token-detection-metrics.spec.js @@ -98,7 +98,7 @@ describe('Token detection event @no-mmi', function () { locale: 'en', chain_id: '0x539', environment_type: 'fullscreen', - is_profile_syncing_enabled: null, + is_profile_syncing_enabled: true, }); assert.deepStrictEqual(events[2].properties, { token_detection_enabled: true, diff --git a/test/e2e/tests/metrics/wallet-created.spec.js b/test/e2e/tests/metrics/wallet-created.spec.js index 890ac9342a8a..fbe80fb595dc 100644 --- a/test/e2e/tests/metrics/wallet-created.spec.js +++ b/test/e2e/tests/metrics/wallet-created.spec.js @@ -87,7 +87,7 @@ describe('Wallet Created Events @no-mmi', function () { locale: 'en', chain_id: '0x539', environment_type: 'fullscreen', - is_profile_syncing_enabled: null, + is_profile_syncing_enabled: true, }); }, ); diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index dc8f38e1168c..d24beb431c1c 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -119,9 +119,7 @@ const inputData = { }; describe('Custom network', function () { - const chainID = '42161'; const networkURL = 'https://arbitrum-mainnet.infura.io'; - const networkNAME = 'Arbitrum One'; const currencySYMBOL = 'ETH'; const blockExplorerURL = 'https://explorer.arbitrum.io'; @@ -456,52 +454,29 @@ describe('Custom network', function () { text: 'Add', }); - // verify network details - const title = await driver.findElement({ - tag: 'span', - text: 'Arbitrum One', - }); - - assert.equal( - await title.getText(), - 'Arbitrum One', - 'Title of popup should be selected network', + const [currencySymbol, networkUrl] = await driver.findElements( + '.definition-list dd', ); - - const [networkName, networkUrl, chainIdElement, currencySymbol] = - await driver.findElements('.definition-list dd'); - assert.equal( - await networkName.getText(), - networkNAME, - 'Network name is not correctly displayed', + await currencySymbol.getText(), + currencySYMBOL, + 'Currency symbol is not correctly displayed', ); assert.equal( await networkUrl.getText(), networkURL, 'Network Url is not correctly displayed', ); - assert.equal( - await chainIdElement.getText(), - chainID.toString(), - 'Chain Id is not correctly displayed', - ); - assert.equal( - await currencySymbol.getText(), - currencySYMBOL, - 'Currency symbol is not correctly displayed', - ); - await driver.clickElement({ tag: 'a', text: 'View all details' }); + await driver.clickElement({ tag: 'a', text: 'See details' }); const networkDetailsLabels = await driver.findElements('dd'); assert.equal( - await networkDetailsLabels[8].getText(), + await networkDetailsLabels[4].getText(), blockExplorerURL, 'Block Explorer URL is not correct', ); - await driver.clickElement({ tag: 'button', text: 'Close' }); await driver.clickElement({ tag: 'button', text: 'Approve' }); // verify network switched diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index 6fc7025f5dbc..ac693361435d 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -1,20 +1,27 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; import FixtureBuilder from '../../fixture-builder'; -import { - defaultGanacheOptions, - importSRPOnboardingFlow, - regularDelayMs, - TEST_SEED_PHRASE, - unlockWallet, - withFixtures, -} from '../../helpers'; +import { defaultGanacheOptions, withFixtures } from '../../helpers'; import { Driver } from '../../webdriver/driver'; import { Mockttp } from '../../mock-e2e'; import { expectMockRequest, expectNoMockRequest, } from '../../helpers/mock-server'; +import EditNetworkModal from '../../page-objects/pages/dialog/edit-network'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingPrivacySettingsPage from '../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import SelectNetwork from '../../page-objects/pages/dialog/select-network'; +import { + loginWithoutBalanceValidation, + loginWithBalanceValidation, +} from '../../page-objects/flows/login.flow'; +import { + completeImportSRPOnboardingFlow, + importSRPOnboardingFlow, +} from '../../page-objects/flows/onboarding.flow'; describe('MultiRpc:', function (this: Suite) { it('should migrate to multi rpc @no-mmi', async function () { @@ -72,37 +79,19 @@ describe('MultiRpc:', function (this: Suite) { testSpecificMock: mockRPCURLAndChainId, }, - async ({ driver }: { driver: Driver }) => { - const password = 'password'; - - await driver.navigate(); + async ({ driver, ganacheServer }) => { + await completeImportSRPOnboardingFlow({ driver }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); - - await driver.delay(regularDelayMs); - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); - - // pin extension walkthrough screen - await driver.findElement('[data-testid="account-menu-icon"]'); - - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - await driver.clickElement( - '[data-testid="network-rpc-name-button-0xa4b1"]', - ); - - const menuItems = await driver.findElements('.select-rpc-url__item'); + await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); // check rpc number - assert.equal(menuItems.length, 2); + await selectNetworkDialog.openNetworkRPC('0xa4b1'); + await selectNetworkDialog.check_networkRPCNumber(2); }, ); }); @@ -173,7 +162,9 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver, mockedEndpoint }) => { - await unlockWallet(driver); + await loginWithoutBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); const usedUrlBeforeSwitch = await mockedEndpoint[1].getSeenRequests(); @@ -189,28 +180,21 @@ describe('MultiRpc:', function (this: Suite) { // check that requests are sent on the background for the rpc https://responsive-rpc.test/ await expectNoMockRequest(driver, mockedEndpoint[0], { timeout: 3000 }); - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - // select second rpc - await driver.clickElement( - '[data-testid="network-rpc-name-button-0xa4b1"]', - ); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.openNetworkRPC('0xa4b1'); + await selectNetworkDialog.check_networkRPCNumber(2); - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); + // select second rpc for Arbitrum network in the network dialog + await selectNetworkDialog.selectRPC('Arbitrum mainnet 2'); + await homePage.check_pageIsLoaded(); + await headerNavbar.clickSwitchNetworkDropDown(); - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + // check that the second rpc is selected in the network dialog + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); const usedUrl = await mockedEndpoint[0].getSeenRequests(); // check the url first request send on the background to the mocked rpc after switch @@ -218,9 +202,6 @@ describe('MultiRpc:', function (this: Suite) { // check that requests are sent on the background for the url https://responsive-rpc.test/ await expectMockRequest(driver, mockedEndpoint[0], { timeout: 3000 }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); }, ); }); @@ -280,53 +261,33 @@ describe('MultiRpc:', function (this: Suite) { testSpecificMock: mockRPCURLAndChainId, }, - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - // Go to Edit Menu - await driver.clickElement( - '[data-testid="network-list-item-options-button-0xa4b1"]', - ); - await driver.clickElement( - '[data-testid="network-list-item-options-edit"]', - ); - await driver.clickElement('[data-testid="test-add-rpc-drop-down"]'); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Save', - tag: 'button', - }); - - // Validate the network was edited - const networkEdited = await driver.isElementPresent({ - text: '“Arbitrum One” was successfully edited!', - }); - assert.equal( - networkEdited, - true, - '“Arbitrum One” was successfully edited!', + async ({ driver, ganacheServer }) => { + await loginWithBalanceValidation(driver, ganacheServer); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + + // go to Edit Menu for Arbitrum network and select the second rpc + await selectNetworkDialog.openNetworkListOptions('0xa4b1'); + await selectNetworkDialog.openEditNetworkModal(); + + const editNetworkModal = new EditNetworkModal(driver); + await editNetworkModal.check_pageIsLoaded(); + await editNetworkModal.selectRPCInEditNetworkModal( + 'Arbitrum mainnet 2', ); - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + // validate the network was successfully edited + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_editNetworkMessageIsDisplayed('Arbitrum One'); + await homePage.closeUseNetworkNotificationModal(); - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); + // check that the second rpc is selected in the network dialog + await headerNavbar.clickSwitchNetworkDropDown(); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); }, ); }); @@ -387,93 +348,41 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver }: { driver: Driver }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); - - await driver.delay(regularDelayMs); - - // go to advanced settigns - await driver.clickElementAndWaitToDisappear({ - text: 'Manage default privacy settings', - }); - - await driver.clickElement({ - text: 'General', - }); - - // open edit modal - await driver.clickElement({ - text: 'arbitrum-mainnet.infura.io', - tag: 'p', - }); - - await driver.clickElement('[data-testid="test-add-rpc-drop-down"]'); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - 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 importSRPOnboardingFlow({ driver }); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, ); + await onboardingPrivacySettingsPage.check_pageIsLoaded(); + await onboardingPrivacySettingsPage.navigateToGeneralSettings(); - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - // Validate the network was edited - const networkEdited = await driver.isElementPresent({ - text: '“Arbitrum One” was successfully edited!', - }); - assert.equal( - networkEdited, - true, - '“Arbitrum One” was successfully edited!', + // open edit network modal during onboarding and select the second rpc + await onboardingPrivacySettingsPage.openEditNetworkModal( + 'Arbitrum One', ); - // 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.clickElement('[data-testid="network-display"]'); - - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); + const editNetworkModal = new EditNetworkModal(driver); + await editNetworkModal.check_pageIsLoaded(); + await editNetworkModal.selectRPCInEditNetworkModal( + 'Arbitrum mainnet 2', + ); + await onboardingPrivacySettingsPage.navigateBackToSettingsPage(); + await onboardingPrivacySettingsPage.check_pageIsLoaded(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + // finish onboarding and check the network successfully edited message is displayed + await onboardingCompletePage.completeOnboarding(); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_editNetworkMessageIsDisplayed('Arbitrum One'); + await homePage.closeUseNetworkNotificationModal(); + + // check that the second rpc is selected in the network dialog + await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); }, ); }); diff --git a/test/e2e/tests/network/switch-network.spec.ts b/test/e2e/tests/network/switch-network.spec.ts new file mode 100644 index 000000000000..a45e634dbbec --- /dev/null +++ b/test/e2e/tests/network/switch-network.spec.ts @@ -0,0 +1,55 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { withFixtures, defaultGanacheOptions } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Ganache } from '../../seeder/ganache'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import HomePage from '../../page-objects/pages/homepage'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import { + switchToNetworkFlow, + searchAndSwitchToNetworkFlow, +} from '../../page-objects/flows/network.flow'; + +describe('Switch network - ', function (this: Suite) { + it('Switch networks to existing and new networks', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + const homePage = new HomePage(driver); + + // Validate the default network is Localhost 8545 + await new HeaderNavbar(driver).check_currentSelectedNetwork( + 'Localhost 8545', + ); + + // Validate the switch network functionality to Ethereum Mainnet + await switchToNetworkFlow(driver, 'Ethereum Mainnet'); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + + // Validate the switch network functionality to test network + await switchToNetworkFlow(driver, 'Localhost 8545', true); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + + // Add Arbitrum network and perform the switch network functionality + await searchAndSwitchToNetworkFlow(driver, 'Arbitrum One'); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + + // Validate the switch network functionality back to Ethereum Mainnet + await switchToNetworkFlow(driver, 'Ethereum Mainnet'); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); + }, + ); + }); +}); diff --git a/test/e2e/tests/notifications/account-syncing/helpers.ts b/test/e2e/tests/notifications/account-syncing/helpers.ts new file mode 100644 index 000000000000..5e2694067eed --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/helpers.ts @@ -0,0 +1,18 @@ +import { isManifestV3 } from '../../../../../shared/modules/mv3.utils'; +import { + completeSRPRevealQuiz, + openSRPRevealQuiz, + tapAndHoldToRevealSRP, +} from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; + +export const IS_ACCOUNT_SYNCING_ENABLED = isManifestV3; + +export const getSRP = async (driver: Driver, password: string) => { + await openSRPRevealQuiz(driver); + await completeSRPRevealQuiz(driver); + await driver.fill('[data-testid="input-password"]', password); + await driver.press('[data-testid="input-password"]', driver.Key.ENTER); + await tapAndHoldToRevealSRP(driver); + return (await driver.findElement('[data-testid="srp_text"]')).getText(); +}; diff --git a/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts b/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts new file mode 100644 index 000000000000..7b9e2378b058 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts @@ -0,0 +1,112 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../../helpers'; +import FixtureBuilder from '../../../fixture-builder'; +import { mockNotificationServices } from '../mocks'; +import { + NOTIFICATIONS_TEAM_IMPORTED_PRIVATE_KEY, + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, +} from '../constants'; +import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; +import { accountsSyncMockResponse } from './mockData'; +import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; + +describe('Account syncing - Import With Private Key @no-mmi', function () { + if (!IS_ACCOUNT_SYNCING_ENABLED) { + return; + } + describe('from inside MetaMask', function () { + it('does not sync accounts imported with private keys', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts( + accountsSyncMockResponse.length, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.openAccountOptionsMenu(); + await accountListPage.addNewImportedAccount( + NOTIFICATIONS_TEAM_IMPORTED_PRIVATE_KEY, + ); + }, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts(2); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/notifications/account-syncing/mockData.ts b/test/e2e/tests/notifications/account-syncing/mockData.ts new file mode 100644 index 000000000000..96e92ecd8491 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/mockData.ts @@ -0,0 +1,12 @@ +export const accountsSyncMockResponse = [ + { + HashedKey: + '997050281e559a2bb40d1c2e73d9f0887cbea1b81ff9dd7815917949e37f4f2f', + Data: '{"v":"1","t":"scrypt","d":"1yC/ZXarV57HbqEZ46nH0JWgXfPl86nTHD7kai2g5gm290FM9tw5QjOaAAwIuQESEE8TIM/J9pIj7nmlGi+BZrevTtK3DXWXwnUQsCP7amKd5Q4gs3EEQgXpA0W+WJUgyElj869rwIv/C6tl5E2pK4j/0EAjMSIm1TGoj9FPohyRgZsOIt8VhZfb7w0GODsjPwPIkN6zazvJ3gAFYFPh7yRtebFs86z3fzqCWZ9zakdCHntchC2oZiaApXR9yzaPlGgnPg==","o":{"N":131072,"r":8,"p":1,"dkLen":16},"saltLen":16}', + }, + { + HashedKey: + 'e53d8feb65b4cf0c339e57bee2a81b155e056622f9192c54b707f928c8a42a7a', + Data: '{"v":"1","t":"scrypt","d":"O7QEtUo7q/jG+UNkD/HOxQARGGRXsGPrLsDlkwDfgfoYlPI0To/M3pJRBlKD0RLEFIPHtHBEA5bv/2izB21VljvhMnhHfo0KgQ+e8Uq1t7grwa+r+ge3qbPNY+w78Xt8GtC+Hkrw5fORKvCn+xjzaCHYV6RxKYbp1TpyCJq7hDrr1XiyL8kqbpE0hAHALrrQOoV9/WXJi9pC5J118kquXx8CNA1P5wO/BXKp1AbryGR6kVW3lsp1sy3lYE/TApa5lTj+","o":{"N":131072,"r":8,"p":1,"dkLen":16},"saltLen":16}', + }, +]; diff --git a/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts new file mode 100644 index 000000000000..992027dd7840 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts @@ -0,0 +1,118 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../../helpers'; +import FixtureBuilder from '../../../fixture-builder'; +import { mockNotificationServices } from '../mocks'; +import { NOTIFICATIONS_TEAM_PASSWORD } from '../constants'; +import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { + completeCreateNewWalletOnboardingFlow, + completeImportSRPOnboardingFlow, +} from '../../../page-objects/flows/onboarding.flow'; +import { getSRP, IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; + +describe('Account syncing - New User @no-mmi', function () { + if (!IS_ACCOUNT_SYNCING_ENABLED) { + return; + } + + describe('from inside MetaMask', function () { + it('syncs after new wallet creation', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + let walletSrp: string; + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + // Create a new wallet + await completeCreateNewWalletOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_PASSWORD, + ); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // Open account menu and validate 1 account is shown + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts(1); + await accountListPage.check_accountDisplayedInAccountList( + 'Account 1', + ); + + // Add a second account + await accountListPage.openAccountOptionsMenu(); + await accountListPage.addNewAccountWithCustomLabel( + 'My Second Account', + ); + + // Set SRP to use for retreival + walletSrp = await getSRP(driver, NOTIFICATIONS_TEAM_PASSWORD); + if (!walletSrp) { + throw new Error('Wallet SRP was not set'); + } + }, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + // Onboard with import flow using SRP from new account created above + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: walletSrp, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // Open account menu and validate the 2 accounts have been retrieved + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + + await accountListPage.check_numberOfAvailableAccounts(2); + + await accountListPage.check_accountDisplayedInAccountList( + 'Account 1', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Account', + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts new file mode 100644 index 000000000000..f9574a27cb10 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts @@ -0,0 +1,180 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../../helpers'; +import FixtureBuilder from '../../../fixture-builder'; +import { mockNotificationServices } from '../mocks'; +import { + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, +} from '../constants'; +import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; +import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import HomePage from '../../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingPrivacySettingsPage from '../../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import { + createNewWalletOnboardingFlow, + importSRPOnboardingFlow, + completeImportSRPOnboardingFlow, +} from '../../../page-objects/flows/onboarding.flow'; +import { getSRP, IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; +import { accountsSyncMockResponse } from './mockData'; + +describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { + if (!IS_ACCOUNT_SYNCING_ENABLED) { + return; + } + describe('from inside MetaMask', function () { + let walletSrp: string; + it('does not sync when profile sync is turned off - previously synced account', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + // Mocks are still set up to ensure that requests are not matched + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await importSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = + new OnboardingPrivacySettingsPage(driver); + await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts(1); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'Account 1', + ); + }, + ); + }); + + it('does not sync when profile sync is turned off - new user', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + // Mocks are still set up to ensure that requests are not matched + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await createNewWalletOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_PASSWORD, + ); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = + new OnboardingPrivacySettingsPage(driver); + await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts(1); + await accountListPage.check_accountDisplayedInAccountList( + 'Account 1', + ); + await accountListPage.addNewAccountWithCustomLabel('New Account'); + // Set SRP to use for retreival + walletSrp = await getSRP(driver, NOTIFICATIONS_TEAM_PASSWORD); + if (!walletSrp) { + throw new Error('Wallet SRP was not set'); + } + }, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + // Mocks are still set up to ensure that requests are not matched + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: walletSrp, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts(1); + await accountListPage.check_accountDisplayedInAccountList( + 'Account 1', + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts new file mode 100644 index 000000000000..31f92520f13e --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts @@ -0,0 +1,214 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../../helpers'; +import FixtureBuilder from '../../../fixture-builder'; +import { mockNotificationServices } from '../mocks'; +import { + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, +} from '../constants'; +import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; +import { accountsSyncMockResponse } from './mockData'; +import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; + +describe('Account syncing - Add Account @no-mmi', function () { + if (!IS_ACCOUNT_SYNCING_ENABLED) { + return; + } + describe('from inside MetaMask', function () { + it('syncs newly added accounts - custom name', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts( + accountsSyncMockResponse.length, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.addNewAccountWithCustomLabel( + 'My third account', + ); + }, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + + const accountSyncResponse = + userStorageMockttpController.paths.get('accounts')?.response; + + await accountListPage.check_numberOfAvailableAccounts( + accountSyncResponse?.length as number, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My third account', + ); + }, + ); + }); + + it('syncs newly added accounts - default name', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts( + accountsSyncMockResponse.length, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.addNewAccountWithDefaultName(); + }, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + + const accountSyncResponse = + userStorageMockttpController.paths.get('accounts')?.response; + + await accountListPage.check_numberOfAvailableAccounts( + accountSyncResponse?.length as number, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'Account 3', + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts new file mode 100644 index 000000000000..45ee3ab23a85 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts @@ -0,0 +1,114 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../../helpers'; +import FixtureBuilder from '../../../fixture-builder'; +import { mockNotificationServices } from '../mocks'; +import { + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, +} from '../constants'; +import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; +import { accountsSyncMockResponse } from './mockData'; +import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; + +describe('Account syncing - Rename Accounts @no-mmi', function () { + if (!IS_ACCOUNT_SYNCING_ENABLED) { + return; + } + describe('from inside MetaMask', function () { + it('syncs renamed account names', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts( + accountsSyncMockResponse.length, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + await accountListPage.openAccountOptionsMenu(); + await accountListPage.changeAccountLabel('My Renamed First Account'); + }, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts( + accountsSyncMockResponse.length, + ); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Renamed First Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts new file mode 100644 index 000000000000..5bebe7220e49 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts @@ -0,0 +1,68 @@ +import { Mockttp } from 'mockttp'; +import { withFixtures } from '../../../helpers'; +import FixtureBuilder from '../../../fixture-builder'; +import { mockNotificationServices } from '../mocks'; +import { + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, +} from '../constants'; +import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; +import { accountsSyncMockResponse } from './mockData'; +import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; + +describe('Account syncing - Onboarding @no-mmi', function () { + if (!IS_ACCOUNT_SYNCING_ENABLED) { + return; + } + describe('from inside MetaMask', function () { + it('retrieves all previously synced accounts', async function () { + const userStorageMockttpController = new UserStorageMockttpController(); + + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await completeImportSRPOnboardingFlow({ + driver, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + const header = new HeaderNavbar(driver); + await header.check_pageIsLoaded(); + await header.openAccountMenu(); + + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_numberOfAvailableAccounts( + accountsSyncMockResponse.length, + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My First Synced Account', + ); + await accountListPage.check_accountDisplayedInAccountList( + 'My Second Synced Account', + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/notifications/constants.ts b/test/e2e/tests/notifications/constants.ts new file mode 100644 index 000000000000..557483174b08 --- /dev/null +++ b/test/e2e/tests/notifications/constants.ts @@ -0,0 +1,10 @@ +// As we rely on profile syncing for most of our features, we need to use the same SRP for all of our tests +export const NOTIFICATIONS_TEAM_SEED_PHRASE = + 'leisure swallow trip elbow prison wait rely keep supply hole general mountain'; +export const NOTIFICATIONS_TEAM_PASSWORD = 'notify_password'; +// You can use the storage key below to generate mock data +export const NOTIFICATIONS_TEAM_STORAGE_KEY = + '0d55d30da233959674d14076737198c05ae3fb8631a17e20d3c28c60dddd82f7'; + +export const NOTIFICATIONS_TEAM_IMPORTED_PRIVATE_KEY = + '0179f7453ff337aba89d04b00085764092cf8c0cfe91d5614c39c2be902ad582'; diff --git a/test/e2e/tests/notifications/mocks.ts b/test/e2e/tests/notifications/mocks.ts index 9161faf2967f..ce2ced3df210 100644 --- a/test/e2e/tests/notifications/mocks.ts +++ b/test/e2e/tests/notifications/mocks.ts @@ -1,15 +1,12 @@ import { Mockttp, RequestRuleBuilder } from 'mockttp'; -import { - AuthenticationController, - UserStorageController, -} from '@metamask/profile-sync-controller'; +import { AuthenticationController } from '@metamask/profile-sync-controller'; import { NotificationServicesController, NotificationServicesPushController, } from '@metamask/notification-services-controller'; +import { UserStorageMockttpController } from '../../helpers/user-storage/userStorageMockttpController'; const AuthMocks = AuthenticationController.Mocks; -const StorageMocks = UserStorageController.Mocks; const NotificationMocks = NotificationServicesController.Mocks; const PushMocks = NotificationServicesPushController.Mocks; @@ -20,32 +17,30 @@ type MockResponse = { }; /** - * E2E mock setup for notification APIs (Auth, Storage, Notifications, Push Notifications, Profile syncing) + * E2E mock setup for notification APIs (Auth, UserStorage, Notifications, Push Notifications, Profile syncing) * * @param server - server obj used to mock our endpoints + * @param userStorageMockttpControllerInstance - optional instance of UserStorageMockttpController, useful if you need persisted user storage between tests */ -export async function mockNotificationServices(server: Mockttp) { +export async function mockNotificationServices( + server: Mockttp, + userStorageMockttpControllerInstance: UserStorageMockttpController = new UserStorageMockttpController(), +) { // Auth mockAPICall(server, AuthMocks.getMockAuthNonceResponse()); mockAPICall(server, AuthMocks.getMockAuthLoginResponse()); mockAPICall(server, AuthMocks.getMockAuthAccessTokenResponse()); // Storage - mockAPICall(server, await StorageMocks.getMockUserStorageGetResponse()); - mockAPICall(server, await StorageMocks.getMockUserStoragePutResponse()); - - // TODO - add better mock responses for other Profile Sync features - // (Account Sync, Network Sync, ...) - server - .forGet(/https:\/\/user-storage\.api\.cx\.metamask\.io\/.*/gu) - ?.thenCallback(() => ({ - statusCode: 404, - })); - server - .forPut(/https:\/\/user-storage\.api\.cx\.metamask\.io\/.*/gu) - ?.thenCallback(() => ({ - statusCode: 204, - })); + if (!userStorageMockttpControllerInstance?.paths.get('accounts')) { + userStorageMockttpControllerInstance.setupPath('accounts', server); + } + if (!userStorageMockttpControllerInstance?.paths.get('networks')) { + userStorageMockttpControllerInstance.setupPath('networks', server); + } + if (!userStorageMockttpControllerInstance?.paths.get('notifications')) { + userStorageMockttpControllerInstance.setupPath('notifications', server); + } // Notifications mockAPICall(server, NotificationMocks.getMockFeatureAnnouncementResponse()); diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js deleted file mode 100644 index de040f825ee6..000000000000 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ /dev/null @@ -1,748 +0,0 @@ -const { strict: assert } = require('assert'); -const { toHex } = require('@metamask/controller-utils'); -const { By } = require('selenium-webdriver'); -const { - TEST_SEED_PHRASE, - convertToHexValue, - withFixtures, - completeCreateNewWalletOnboardingFlow, - completeImportSRPOnboardingFlow, - importSRPOnboardingFlow, - importWrongSRPOnboardingFlow, - testSRPDropdownIterations, - locateAccountBalanceDOM, - defaultGanacheOptions, - WALLET_PASSWORD, - onboardingBeginCreateNewWallet, - onboardingChooseMetametricsOption, - onboardingCreatePassword, - onboardingRevealAndConfirmSRP, - onboardingCompleteWalletCreation, - regularDelayMs, - unlockWallet, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); -const { - FirstTimeFlowType, -} = require('../../../../shared/constants/onboarding'); - -describe('MetaMask onboarding @no-mmi', function () { - const wrongSeedPhrase = - 'test test test test test test test test test test test test'; - const wrongTestPassword = 'test test test test'; - - const ganacheOptions2 = { - accounts: [ - { - secretKey: - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', - balance: convertToHexValue(10000000000000000000), - }, - ], - }; - - it('Clicks create a new wallet, accepts a secure password, reveals the Secret Recovery Phrase, confirm SRP', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await completeCreateNewWalletOnboardingFlow(driver, WALLET_PASSWORD); - - const homePage = await driver.findElement('.home__main-view'); - const homePageDisplayed = await homePage.isDisplayed(); - - assert.equal(homePageDisplayed, true); - }, - ); - }); - - it('Clicks import a new wallet, accepts a secure password, reveals the Secret Recovery Phrase, confirm SRP', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await completeImportSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - const homePage = await driver.findElement('.home__main-view'); - const homePageDisplayed = await homePage.isDisplayed(); - - assert.equal(homePageDisplayed, true); - }, - ); - }); - - it('User import wrong Secret Recovery Phrase', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await importWrongSRPOnboardingFlow(driver, wrongSeedPhrase); - - const confirmSeedPhrase = await driver.findElement( - '[data-testid="import-srp-confirm"]', - ); - - assert.equal(await confirmSeedPhrase.isEnabled(), false); - }, - ); - }); - - it('Check if user select different type of secret recovery phrase', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - // accept terms of use - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - - // welcome - await driver.clickElement('[data-testid="onboarding-import-wallet"]'); - - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - const dropdownElement = await driver.findElement( - '.import-srp__number-of-words-dropdown', - ); - await dropdownElement.click(); - const options = await dropdownElement.findElements(By.css('option')); - - const iterations = options.length; - - await testSRPDropdownIterations(options, driver, iterations); - - const finalFormFields = await driver.findElements( - '.import-srp__srp-word-label', - ); - const expectedFinalNumFields = 24; // The last iteration will have 24 fields - const actualFinalNumFields = finalFormFields.length; - assert.equal(actualFinalNumFields, expectedFinalNumFields); - }, - ); - }); - - it('User enters the wrong password during password creation', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - await driver.clickElement('[data-testid="onboarding-create-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // Fill in confirm password field with incorrect password - await driver.fill( - '[data-testid="create-password-new"]', - WALLET_PASSWORD, - ); - await driver.fill( - '[data-testid="create-password-confirm"]', - wrongTestPassword, - ); - - // Check that the error message is displayed for the password fields - await driver.isElementPresent( - { text: "Passwords don't match", tag: 'h6' }, - true, - ); - - // Check that the "Confirm Password" button is disabled - const confirmPasswordButton = await driver.findElement( - '[data-testid="create-password-wallet"]', - ); - assert.equal(await confirmPasswordButton.isEnabled(), false); - }, - ); - }); - - it('Verify that the user has been redirected to the correct page after importing their wallet', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - // Verify site - assert.equal( - await driver.isElementPresent({ - text: 'Your wallet is ready', - tag: 'h2', - }), - true, - ); - }, - ); - }); - - it('Verify that the user has been redirected to the correct page after creating a password for their new wallet', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - - await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); - await driver.clickElement('[data-testid="onboarding-create-wallet"]'); - - // metrics - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // Fill in confirm password field with correct password - await driver.fill( - '[data-testid="create-password-new"]', - WALLET_PASSWORD, - ); - await driver.fill( - '[data-testid="create-password-confirm"]', - WALLET_PASSWORD, - ); - await driver.clickElement('[data-testid="create-password-terms"]'); - await driver.clickElement('[data-testid="create-password-wallet"]'); - - // Verify site - assert.equal( - await driver.isElementPresent({ - text: 'Secure your wallet', - tag: 'h2', - }), - true, - ); - }, - ); - }); - - it('User can add custom network during onboarding', async function () { - const networkName = 'Localhost 8546'; - const networkUrl = 'http://127.0.0.1:8546'; - const currencySymbol = 'ETH'; - const port = 8546; - const chainId = 1338; - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: { - ...defaultGanacheOptions, - concurrent: [{ port, chainId, ganacheOptions2 }], - }, - title: this.test.fullTitle(), - }, - - async ({ driver, secondaryGanacheServer }) => { - try { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - await driver.clickElement({ - text: 'Manage default privacy settings', - tag: 'button', - }); - - await driver.clickElement({ - text: 'General', - }); - await driver.clickElement({ text: 'Add a network' }); - - await driver.waitForSelector( - '.multichain-network-list-menu-content-wrapper__dialog', - ); - - 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, - ); - - // 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"]', - ); - - 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('.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 default settings', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); - - 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"]'); - - // 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', - css: '.mm-banner-alert', - }); - }, - ); - }); - - it("doesn't make any network requests to infura before onboarding is completed", async function () { - async function mockInfura(mockServer) { - const infuraUrl = - 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; - const sampleAddress = '1111111111111111111111111111111111111111'; - - return [ - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_blockNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x1', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBalance' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x0', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: {}, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_call' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: `0x000000000000000000000000${sampleAddress}`, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'net_version' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, - }; - }), - ]; - } - - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }) - .withNetworkControllerOnMainnet() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockInfura, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - const password = 'password'; - - await driver.navigate(); - - await onboardingBeginCreateNewWallet(driver); - await onboardingChooseMetametricsOption(driver, false); - await onboardingCreatePassword(driver, password); - await onboardingRevealAndConfirmSRP(driver); - await onboardingCompleteWalletCreation(driver); - - // pin extension walkthrough screen - await driver.clickElement('[data-testid="pin-extension-next"]'); - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - const isPending = await mockedEndpoint.isPending(); - assert.equal( - isPending, - true, - `${mockedEndpoints[i]} mock should still be pending before onboarding`, - ); - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length, - 0, - `${mockedEndpoints[i]} should make no requests before onboarding`, - ); - } - - await driver.clickElement('[data-testid="pin-extension-done"]'); - // requests happen here! - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - - await driver.wait(async () => { - const isPending = await mockedEndpoint.isPending(); - return isPending === false; - }, driver.timeout); - - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length > 0, - true, - `${mockedEndpoints[i]} should make requests after onboarding`, - ); - } - }, - ); - }); - - it("doesn't make any network requests to infura before onboarding by import is completed", async function () { - async function mockInfura(mockServer) { - const infuraUrl = - 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; - const sampleAddress = '1111111111111111111111111111111111111111'; - - return [ - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_blockNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x1', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBalance' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: '0x0', - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: {}, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'eth_call' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: '1111111111111111', - result: `0x000000000000000000000000${sampleAddress}`, - }, - }; - }), - await mockServer - .forPost(infuraUrl) - .withJsonBodyIncluding({ method: 'net_version' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, - }; - }), - ]; - } - - await withFixtures( - { - fixtures: new FixtureBuilder({ onboarding: true }) - .withNetworkControllerOnMainnet() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockInfura, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); - - await driver.delay(regularDelayMs); - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length, - 0, - `${mockedEndpoints[i]} should make no requests before onboarding`, - ); - } - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); - - // pin extension walkthrough screen - await driver.findElement('[data-testid="account-menu-icon"]'); - // requests happen here! - - for (let i = 0; i < mockedEndpoints.length; i += 1) { - const mockedEndpoint = await mockedEndpoints[i]; - - await driver.wait(async () => { - const isPending = await mockedEndpoint.isPending(); - return isPending === false; - }, driver.timeout); - - const requests = await mockedEndpoint.getSeenRequests(); - - assert.equal( - requests.length > 0, - true, - `${mockedEndpoints[i]} should make requests after onboarding`, - ); - } - }, - ); - }); - - it('Provides an onboarding path for a user who has restored their account from state persistence failure', async function () { - // We don't use onboarding:true here because we want there to be a vault, - // simulating what will happen when a user eventually restores their vault - // during a state persistence failure. Instead, we set the - // firstTimeFlowType to 'restore' and completedOnboarding to false. as well - // as some other first time state options to get us into an onboarding - // state similar to a new state tree. - await withFixtures( - { - fixtures: new FixtureBuilder() - .withOnboardingController({ - completedOnboarding: false, - firstTimeFlowType: FirstTimeFlowType.restore, - seedPhraseBackedUp: null, - }) - .withMetaMetricsController({ - participateInMetaMetrics: null, - metaMetricsId: null, - }) - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // First screen we should be on is MetaMetrics - assert.equal( - await driver.isElementPresent({ - text: 'Help us improve MetaMask', - tag: 'h2', - }), - true, - 'First screen should be MetaMetrics', - ); - - // select no thanks - await driver.clickElement('[data-testid="metametrics-no-thanks"]'); - - // Next should be Secure your wallet screen - assert.equal( - await driver.isElementPresent({ - text: 'Secure your wallet', - tag: 'h2', - }), - true, - ); - }, - ); - }); -}); diff --git a/test/e2e/tests/onboarding/onboarding.spec.ts b/test/e2e/tests/onboarding/onboarding.spec.ts new file mode 100644 index 000000000000..9ea81f040998 --- /dev/null +++ b/test/e2e/tests/onboarding/onboarding.spec.ts @@ -0,0 +1,275 @@ +import { + convertToHexValue, + TEST_SEED_PHRASE, + WALLET_PASSWORD, + withFixtures, +} from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; +import HomePage from '../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingMetricsPage from '../../page-objects/pages/onboarding/onboarding-metrics-page'; +import OnboardingPasswordPage from '../../page-objects/pages/onboarding/onboarding-password-page'; +import OnboardingPrivacySettingsPage from '../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import OnboardingSrpPage from '../../page-objects/pages/onboarding/onboarding-srp-page'; +import SecureWalletPage from '../../page-objects/pages/onboarding/secure-wallet-page'; +import StartOnboardingPage from '../../page-objects/pages/onboarding/start-onboarding-page'; +import { loginWithoutBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + completeCreateNewWalletOnboardingFlow, + completeImportSRPOnboardingFlow, + importSRPOnboardingFlow, +} from '../../page-objects/flows/onboarding.flow'; +import { switchToNetworkFlow } from '../../page-objects/flows/network.flow'; + +describe('MetaMask onboarding @no-mmi', function () { + const ganacheOptions2 = { + accounts: [ + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: convertToHexValue(10000000000000000000), + }, + ], + }; + + it('Creates a new wallet, sets up a secure password, and completes the onboarding process', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await completeCreateNewWalletOnboardingFlow(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + }, + ); + }); + + it('Imports an existing wallet, sets up a secure password, and completes the onboarding process', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await completeImportSRPOnboardingFlow({ driver }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + }, + ); + }); + + it('Attempts to import a wallet with an incorrect Secret Recovery Phrase and verifies the error message', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + const wrongSeedPhrase = + 'test test test test test test test test test test test test'; + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickImportWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingSrpPage = new OnboardingSrpPage(driver); + await onboardingSrpPage.check_pageIsLoaded(); + await onboardingSrpPage.fillSrp(wrongSeedPhrase); + + // check the wrong SRP warning message is displayed + await onboardingSrpPage.check_wrongSrpWarningMessage(); + await onboardingSrpPage.check_confirmSrpButtonIsDisabled(); + }, + ); + }); + + it('Verifies the functionality of selecting different Secret Recovery Phrase word counts', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickImportWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingSrpPage = new OnboardingSrpPage(driver); + await onboardingSrpPage.check_pageIsLoaded(); + await onboardingSrpPage.check_srpDropdownIterations(); + }, + ); + }); + + it('Verifies error handling when entering an incorrect password during wallet creation', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + const wrongTestPassword = 'test test test test'; + await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); + await startOnboardingPage.check_pageIsLoaded(); + await startOnboardingPage.checkTermsCheckbox(); + await startOnboardingPage.clickCreateWalletButton(); + + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.fillWalletPassword( + WALLET_PASSWORD, + wrongTestPassword, + ); + + // check the incorrect password warning message is displayed + await onboardingPasswordPage.check_incorrectPasswordWarningMessageIsDisplayed(); + await onboardingPasswordPage.check_confirmPasswordButtonIsDisabled(); + }, + ); + }); + + it('User can add custom network during onboarding', async function () { + const networkName = 'Localhost 8546'; + const networkUrl = 'http://127.0.0.1:8546'; + const currencySymbol = 'ETH'; + const port = 8546; + const chainId = 1338; + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions: { + concurrent: [{ port, chainId, ganacheOptions2 }], + }, + title: this.test?.fullTitle(), + }, + async ({ driver, secondaryGanacheServer }) => { + await importSRPOnboardingFlow({ + driver, + seedPhrase: TEST_SEED_PHRASE, + }); + + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, + ); + await onboardingPrivacySettingsPage.addCustomNetwork( + networkName, + chainId, + currencySymbol, + networkUrl, + ); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await switchToNetworkFlow(driver, networkName); + await homePage.check_addNetworkMessageIsDisplayed(networkName); + + // Check the correct balance for the custom network is displayed + if (secondaryGanacheServer && Array.isArray(secondaryGanacheServer)) { + await homePage.check_ganacheBalanceIsDisplayed( + secondaryGanacheServer[0], + ); + } else { + throw new Error('Custom network Ganache server not available'); + } + }, + ); + }); + + it('User can turn off basic functionality in default settings', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await importSRPOnboardingFlow({ driver }); + + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, + ); + await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + // check the basic functionality is off warning message is displayed + await homePage.check_basicFunctionalityOffWarnigMessageIsDisplayed(); + }, + ); + }); + + it('Provides an onboarding path for a user who has restored their account from state persistence failure', async function () { + // We don't use onboarding: true here because we want there to be a vault, + // simulating what will happen when a user eventually restores their vault + // during a state persistence failure. Instead, we set the + // firstTimeFlowType to 'restore' and completedOnboarding to false. as well + // as some other first time state options to get us into an onboarding + // state similar to a new state tree. + await withFixtures( + { + fixtures: new FixtureBuilder() + .withOnboardingController({ + completedOnboarding: false, + firstTimeFlowType: FirstTimeFlowType.restore, + seedPhraseBackedUp: null, + }) + .withMetaMetricsController({ + participateInMetaMetrics: null, + metaMetricsId: null, + }) + .build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithoutBalanceValidation(driver); + // First screen we should be on is MetaMetrics + const onboardingMetricsPage = new OnboardingMetricsPage(driver); + await onboardingMetricsPage.check_pageIsLoaded(); + await onboardingMetricsPage.clickNoThanksButton(); + + // Next screen should be Secure your wallet screen + const secureWalletPage = new SecureWalletPage(driver); + await secureWalletPage.check_pageIsLoaded(); + }, + ); + }); +}); diff --git a/test/e2e/tests/petnames/petnames-signatures.spec.js b/test/e2e/tests/petnames/petnames-signatures.spec.js index ba6cf7642c59..6c472901057e 100644 --- a/test/e2e/tests/petnames/petnames-signatures.spec.js +++ b/test/e2e/tests/petnames/petnames-signatures.spec.js @@ -46,7 +46,7 @@ async function installNameLookupSnap(driver) { // Confirm Install Modal await driver.clickElement({ - text: 'Install', + text: 'Confirm', tag: 'button', }); @@ -173,9 +173,7 @@ describe('Petnames - Signatures', function () { ); }); - // TODO(dbrans): Re-enable this test when name-lookup endowment is in stable. - // eslint-disable-next-line mocha/no-skipped-tests - it.skip('can propose names using installed snaps', async function () { + it('can propose names using installed snaps', async function () { await withFixtures( { dapp: true, diff --git a/test/e2e/tests/phishing-controller/phishing-detection.spec.js b/test/e2e/tests/phishing-controller/phishing-detection.spec.js index ac9a6d8461d2..ad199cea1e70 100644 --- a/test/e2e/tests/phishing-controller/phishing-detection.spec.js +++ b/test/e2e/tests/phishing-controller/phishing-detection.spec.js @@ -58,8 +58,14 @@ describe('Phishing Detection', function () { await unlockWallet(driver); await openDapp(driver); await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); + + // we need to wait for this selector to mitigate a race condition on the phishing page site + // see more here https://github.com/MetaMask/phishing-warning/pull/173 + await driver.waitForSelector({ + testId: 'unsafe-continue-loaded', + }); await driver.clickElement({ - text: 'continue to the site.', + text: 'Proceed anyway', }); await driver.wait(until.titleIs(WINDOW_TITLES.TestDApp), 10000); }, @@ -103,10 +109,15 @@ describe('Phishing Detection', function () { } await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); + + // we need to wait for this selector to mitigate a race condition on the phishing page site + // see more here https://github.com/MetaMask/phishing-warning/pull/173 + await driver.waitForSelector({ + testId: 'unsafe-continue-loaded', + }); await driver.clickElement({ - text: 'continue to the site.', + text: 'Proceed anyway', }); - await driver.wait(until.titleIs(WINDOW_TITLES.TestDApp), 10000); }; } @@ -169,8 +180,14 @@ describe('Phishing Detection', function () { text: 'Open this warning in a new tab', }); await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); + + // we need to wait for this selector to mitigate a race condition on the phishing page site + // see more here https://github.com/MetaMask/phishing-warning/pull/173 + await driver.waitForSelector({ + testId: 'unsafe-continue-loaded', + }); await driver.clickElement({ - text: 'continue to the site.', + text: 'Proceed anyway', }); // We don't really know what we're going to see at this blocked site, so a waitAtLeast guard of 1000ms is the best choice @@ -253,7 +270,7 @@ describe('Phishing Detection', function () { ); }); - it('should open a new extension expanded view when clicking back to safety button', async function () { + it('should open MetaMask Portfolio when clicking back to safety button', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), @@ -290,11 +307,10 @@ describe('Phishing Detection', function () { text: 'Back to safety', }); - // Ensure we're redirected to wallet home page - const homePage = await driver.findElement('.home__main-view'); - const homePageDisplayed = await homePage.isDisplayed(); + const currentUrl = await driver.getCurrentUrl(); + const expectedPortfolioUrl = `https://portfolio.metamask.io/?metamaskEntry=phishing_page_portfolio_button`; - assert.equal(homePageDisplayed, true); + assert.equal(currentUrl, expectedPortfolioUrl); }, ); }); diff --git a/test/e2e/tests/ppom/constants.ts b/test/e2e/tests/ppom/constants.ts new file mode 100644 index 000000000000..7794e8738a76 --- /dev/null +++ b/test/e2e/tests/ppom/constants.ts @@ -0,0 +1,5 @@ +export const SECURITY_ALERTS_DEV_API_BASE_URL = + 'https://security-alerts.dev-api.cx.metamask.io'; + +export const SECURITY_ALERTS_PROD_API_BASE_URL = + 'https://security-alerts.api.cx.metamask.io'; diff --git a/test/e2e/tests/ppom/ppom-blockaid-alert-erc20-transfer.spec.js b/test/e2e/tests/ppom/ppom-blockaid-alert-erc20-transfer.spec.js index 4122695dfb50..4f6fcf819f94 100644 --- a/test/e2e/tests/ppom/ppom-blockaid-alert-erc20-transfer.spec.js +++ b/test/e2e/tests/ppom/ppom-blockaid-alert-erc20-transfer.spec.js @@ -1,190 +1,92 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { WINDOW_TITLES, defaultGanacheOptions, - openDapp, unlockWallet, withFixtures, } = require('../../helpers'); +const { SECURITY_ALERTS_PROD_API_BASE_URL } = require('./constants'); const { mockServerJsonRpc } = require('./mocks/mock-server-json-rpc'); -const bannerAlertSelector = '[data-testid="security-provider-banner-alert"]'; +const SELECTED_ADDRESS = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; -const selectedAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; -const selectedAddressWithoutPrefix = '5cfe73b6021e818b776b421b1c4db2474086a7e1'; - -const CONTRACT_ADDRESS = { - BalanceChecker: '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39', - FiatTokenV2_1: '0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf', - OffChainOracle: '0x52cbe0f49ccdd4dc6e9c13bab024eabd2842045b', - USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', -}; +const CONTRACT_ADDRESS_USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; async function mockInfura(mockServer) { await mockServerJsonRpc(mockServer, [ ['eth_blockNumber'], - [ - 'eth_call', - { - methodResultVariant: 'balanceChecker', - params: [{ to: CONTRACT_ADDRESS.BalanceChecker }], - }, - ], - [ - 'eth_call', - { - methodResultVariant: 'offchainOracle', - params: [{ to: CONTRACT_ADDRESS.OffChainOracle }], - }, - ], - [ - 'eth_call', - { - methodResultVariant: 'balance', - params: [ - { - accessList: [], - data: `0x70a08231000000000000000000000000${selectedAddressWithoutPrefix}`, - to: CONTRACT_ADDRESS.USDC, - }, - ], - }, - ], + ['eth_call'], ['eth_estimateGas'], ['eth_feeHistory'], ['eth_gasPrice'], ['eth_getBalance'], ['eth_getBlockByNumber'], - [ - 'eth_getCode', - { - methodResultVariant: 'USDC', - params: [CONTRACT_ADDRESS.USDC], - }, - ], + ['eth_getCode'], ['eth_getTransactionCount'], ]); +} - await mockServer - .forPost() - .withJsonBodyIncluding({ - method: 'debug_traceCall', - params: [{ accessList: [], data: '0x00000000' }], - }) - .thenCallback(async (req) => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: (await req.body.getJson()).id, - result: { - calls: [ - { - error: 'execution reverted', - from: CONTRACT_ADDRESS.USDC, - gas: '0x1d55c2c7', - gasUsed: '0xf0', - input: '0x00000000', - to: CONTRACT_ADDRESS.FiatTokenV2_1, - type: 'DELEGATECALL', - value: '0x0', - }, - ], - error: 'execution reverted', - from: '0x0000000000000000000000000000000000000000', - gas: '0x1dcd6500', - gasUsed: '0x6f79', - input: '0x00000000', - to: CONTRACT_ADDRESS.USDC, - type: 'CALL', - value: '0x0', - }, - }, - }; - }); +const maliciousTransferAlert = { + block: 1, + result_type: 'Malicious', + reason: 'transfer_farming', + description: + 'Transfer to 0x5fbdb2315678afecb367f032d93f642f64180aa3, classification: A known malicious address is involved in the transaction', + features: ['A known malicious address is involved in the transaction'], +}; - await mockServer - .forPost() +async function mockRequest(server, response) { + await server + .forPost(`${SECURITY_ALERTS_PROD_API_BASE_URL}/validate/0x1`) .withJsonBodyIncluding({ - method: 'debug_traceCall', - params: [{ from: selectedAddress }], - }) - .thenCallback(async (req) => { - const mockFakePhishingAddress = - '5fbdb2315678afecb367f032d93f642f64180aa3'; - - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: (await req.body.getJson()).id, - result: { - calls: [ - { - from: CONTRACT_ADDRESS.USDC, - gas: '0x2923d', - gasUsed: '0x4cac', - input: `0xa9059cbb000000000000000000000000${mockFakePhishingAddress}0000000000000000000000000000000000000000000000000000000000000064`, - logs: [ - { - address: CONTRACT_ADDRESS.USDC, - data: '0x0000000000000000000000000000000000000000000000000000000000000064', - topics: [ - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - `0x000000000000000000000000${selectedAddressWithoutPrefix}`, - `0x000000000000000000000000${mockFakePhishingAddress}`, - ], - }, - ], - output: - '0x0000000000000000000000000000000000000000000000000000000000000001', - to: CONTRACT_ADDRESS.FiatTokenV2_1, - type: 'DELEGATECALL', - value: '0x0', - }, - ], - from: selectedAddress, - gas: '0x30d40', - gasUsed: '0xbd69', - input: `0xa9059cbb000000000000000000000000${mockFakePhishingAddress}0000000000000000000000000000000000000000000000000000000000000064`, - output: - '0x0000000000000000000000000000000000000000000000000000000000000001', - to: CONTRACT_ADDRESS.USDC, - type: 'CALL', - value: '0x0', - }, + method: 'eth_sendTransaction', + params: [ + { + from: SELECTED_ADDRESS, + data: '0xa9059cbb0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa30000000000000000000000000000000000000000000000000000000000000064', + to: CONTRACT_ADDRESS_USDC, + value: '0x0', }, - }; - }); + ], + }) + .thenJson(201, response); +} + +async function mockInfuraWithMaliciousResponses(mockServer) { + await mockInfura(mockServer); + + await mockRequest(mockServer, maliciousTransferAlert); } describe('PPOM Blockaid Alert - Malicious ERC20 Transfer @no-mmi', function () { - // eslint-disable-next-line mocha/no-skipped-tests - it.skip('should show banner alert', async function () { + it('should show banner alert', async function () { + // we need to use localhost instead of the ip + // see issue: https://github.com/MetaMask/MetaMask-planning/issues/3560 await withFixtures( { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerOnMainnet() - .withPermissionControllerConnectedToTestDapp() + .withPermissionControllerConnectedToTestDapp({ + useLocalhostHostname: true, + }) .withPreferencesController({ securityAlertsEnabled: true, }) .build(), defaultGanacheOptions, - testSpecificMock: mockInfura, + testSpecificMock: mockInfuraWithMaliciousResponses, title: this.test.fullTitle(), }, async ({ driver }) => { const expectedTitle = 'This is a deceptive request'; const expectedDescription = - 'If you approve this request, you might lose your assets.'; + 'If you approve this request, a third party known for scams will take all your assets.'; await unlockWallet(driver); - await openDapp(driver); + await driver.openNewPage('http://localhost:8080'); // Click TestDapp button to send JSON-RPC request await driver.clickElement('#maliciousERC20TransferButton'); @@ -195,20 +97,15 @@ describe('PPOM Blockaid Alert - Malicious ERC20 Transfer @no-mmi', function () { await driver.assertElementNotPresent('.loading-indicator'); - const bannerAlertFoundByTitle = await driver.findElement({ - css: bannerAlertSelector, + await driver.waitForSelector({ + css: '.mm-text--body-lg-medium', text: expectedTitle, }); - const bannerAlertText = await bannerAlertFoundByTitle.getText(); - - assert( - bannerAlertFoundByTitle, - `Banner alert not found. Expected Title: ${expectedTitle} \nExpected reason: transfer_farming\n`, - ); - assert( - bannerAlertText.includes(expectedDescription), - `Unexpected banner alert description. Expected: ${expectedDescription} \nExpected reason: transfer_farming\n`, - ); + + await driver.waitForSelector({ + css: '.mm-text--body-md', + text: expectedDescription, + }); }, ); }); diff --git a/test/e2e/tests/ppom/ppom-blockaid-alert-networks-support.spec.js b/test/e2e/tests/ppom/ppom-blockaid-alert-networks-support.spec.js index 719f8cbdc16b..f40fd68f9566 100644 --- a/test/e2e/tests/ppom/ppom-blockaid-alert-networks-support.spec.js +++ b/test/e2e/tests/ppom/ppom-blockaid-alert-networks-support.spec.js @@ -114,7 +114,7 @@ describe('PPOM Blockaid Alert - Multiple Networks Support @no-mmi', function () text: 'Add', }); - await driver.clickElement({ tag: 'a', text: 'View all details' }); + await driver.clickElement({ tag: 'a', text: 'See details' }); await driver.clickElement({ tag: 'button', text: 'Close' }); await driver.clickElement({ tag: 'button', text: 'Approve' }); diff --git a/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js b/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js index 8f3e7a657716..c1c7323671f5 100644 --- a/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js +++ b/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js @@ -6,7 +6,9 @@ const { withFixtures, sendScreenToConfirmScreen, logInWithBalanceValidation, + WINDOW_TITLES, } = require('../../helpers'); +const { SECURITY_ALERTS_PROD_API_BASE_URL } = require('./constants'); const { mockServerJsonRpc } = require('./mocks/mock-server-json-rpc'); const bannerAlertSelector = '[data-testid="security-provider-banner-alert"]'; @@ -17,6 +19,18 @@ const expectedMaliciousTitle = 'This is a deceptive request'; const expectedMaliciousDescription = 'If you approve this request, a third party known for scams will take all your assets.'; +const SEND_REQUEST_BASE_MOCK = { + method: 'eth_sendTransaction', + params: [ + { + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + data: '0x', + to: mockMaliciousAddress, + value: '0xde0b6b3a7640000', + }, + ], +}; + async function mockInfura(mockServer) { await mockServerJsonRpc(mockServer, [ ['eth_blockNumber'], @@ -31,87 +45,63 @@ async function mockInfura(mockServer) { ]); } +async function mockRequest(server, request, response) { + await server + .forPost(`${SECURITY_ALERTS_PROD_API_BASE_URL}/validate/0x1`) + .withJsonBodyIncluding(request) + .thenJson(response.statusCode ?? 201, response); +} + async function mockInfuraWithBenignResponses(mockServer) { await mockInfura(mockServer); - await mockServer - .forPost() - .withJsonBodyIncluding({ - method: 'debug_traceCall', - }) - .thenCallback(async (req) => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: (await req.body.getJson()).id, - result: { - type: 'CALL', - from: '0x0000000000000000000000000000000000000000', - to: '0xd46e8dd67c5d32be8058bb8eb970870f07244567', - value: '0xde0b6b3a7640000', - gas: '0x16c696eb7', - gasUsed: '0x0', - input: '0x', - output: '0x', - }, - }, - }; - }); + await mockRequest(mockServer, SEND_REQUEST_BASE_MOCK, { + block: 20733513, + result_type: 'Benign', + reason: '', + description: '', + features: [], + }); } async function mockInfuraWithMaliciousResponses(mockServer) { await mockInfura(mockServer); - await mockServer - .forPost() - .withJsonBodyIncluding({ - method: 'debug_traceCall', - params: [{ accessList: [], data: '0x00000000' }], - }) - .thenCallback(async (req) => { - return { - statusCode: 200, - json: { - jsonrpc: '2.0', - id: (await req.body.getJson()).id, - result: { - calls: [ - { - error: 'execution reverted', - from: '0x0000000000000000000000000000000000000000', - gas: '0x1d55c2cb', - gasUsed: '0x39c', - input: '0x00000000', - to: mockMaliciousAddress, - type: 'DELEGATECALL', - value: '0x0', - }, - ], - error: 'execution reverted', - from: '0x0000000000000000000000000000000000000000', - gas: '0x1dcd6500', - gasUsed: '0x721e', - input: '0x00000000', - to: mockMaliciousAddress, - type: 'CALL', - value: '0x0', - }, - }, - }; - }); + await mockRequest(mockServer, SEND_REQUEST_BASE_MOCK, { + block: 20733277, + result_type: 'Malicious', + reason: 'transfer_farming', + description: '', + features: ['Interaction with a known malicious address'], + }); } async function mockInfuraWithFailedResponses(mockServer) { await mockInfura(mockServer); + await mockRequest( + mockServer, + { + ...SEND_REQUEST_BASE_MOCK, + params: [ + { + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + data: '0x', + to: '0xb8c77482e45f1f44de1745f52c74426c631bdd52', + value: '0xf43fc2c04ee0000', + }, + ], + }, + { statusCode: 500, message: 'Internal server error' }, + ); + + // Retained this mock to support fallback to the local PPOM await mockServer - .forPost() - .withJsonBodyIncluding({ - method: 'debug_traceCall', - params: [{ accessList: [], data: '0x00000000' }], - }) + .forGet( + 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json', + ) .thenCallback(() => { + console.log('mocked ppom_version.json'); return { statusCode: 500, }; @@ -145,7 +135,7 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { await logInWithBalanceValidation(driver); await sendScreenToConfirmScreen(driver, mockBenignAddress, '1'); - // await driver.delay(100000) + const isPresent = await driver.isElementPresent(bannerAlertSelector); assert.equal(isPresent, false, `Banner alert unexpectedly found.`); }, @@ -159,10 +149,15 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { */ it('should show security alerts for malicious requests', async function () { await withFixtures( + // we need to use localhost instead of the ip + // see issue: https://github.com/MetaMask/MetaMask-planning/issues/3560 { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerOnMainnet() + .withPermissionControllerConnectedToTestDapp({ + useLocalhostHostname: true, + }) .withPreferencesController({ securityAlertsEnabled: true, }) @@ -175,29 +170,25 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { async ({ driver }) => { await logInWithBalanceValidation(driver); - await sendScreenToConfirmScreen(driver, mockMaliciousAddress, '1'); + await driver.openNewPage('http://localhost:8080'); - // Find element by title - const bannerAlertFoundByTitle = await driver.findElement({ - css: bannerAlertSelector, + await driver.clickElement('#maliciousRawEthButton'); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: '.mm-text--body-lg-medium', text: expectedMaliciousTitle, }); - const bannerAlertText = await bannerAlertFoundByTitle.getText(); - assert( - bannerAlertFoundByTitle, - `Banner alert not found. Expected Title: ${expectedMaliciousTitle}`, - ); - assert( - bannerAlertText.includes(expectedMaliciousDescription), - `Unexpected banner alert description. Expected: ${expectedMaliciousDescription}`, - ); + await driver.waitForSelector({ + css: '.mm-text--body-md', + text: expectedMaliciousDescription, + }); }, ); }); - // eslint-disable-next-line mocha/no-skipped-tests - it.skip('should show "Request may not be safe" if the PPOM request fails to check transaction', async function () { + it('should show "Be careful" if the PPOM request fails to check transaction', async function () { await withFixtures( { dapp: true, @@ -220,8 +211,7 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', '1.1', ); - // await driver.delay(100000) - const expectedTitle = 'Request may not be safe'; + const expectedTitle = 'Be careful'; const bannerAlert = await driver.findElement({ css: bannerAlertSelector, diff --git a/test/e2e/tests/ppom/ppom-blockaid-alert-trade-order-farming.spec.js b/test/e2e/tests/ppom/ppom-blockaid-alert-trade-order-farming.spec.js index 8f2debc7b4f2..ac17614bc5af 100644 --- a/test/e2e/tests/ppom/ppom-blockaid-alert-trade-order-farming.spec.js +++ b/test/e2e/tests/ppom/ppom-blockaid-alert-trade-order-farming.spec.js @@ -1,17 +1,13 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { WINDOW_TITLES, defaultGanacheOptions, - openDapp, unlockWallet, withFixtures, } = require('../../helpers'); const { mockServerJsonRpc } = require('./mocks/mock-server-json-rpc'); -const bannerAlertSelector = '[data-testid="security-provider-banner-alert"]'; - const CONTRACT_ADDRESS = { WrappedEther: 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', OffchainOracle: '0x52cbe0f49ccdd4dc6e9c13bab024eabd2842045b', @@ -90,14 +86,17 @@ async function mockInfura(mockServer) { } describe('PPOM Blockaid Alert - Set Trade farming order @no-mmi', function () { - // eslint-disable-next-line mocha/no-skipped-tests - it.skip('should show banner alert', async function () { + it('should show banner alert', async function () { + // we need to use localhost instead of the ip + // see issue: https://github.com/MetaMask/MetaMask-planning/issues/3560 await withFixtures( { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerOnMainnet() - .withPermissionControllerConnectedToTestDapp() + .withPermissionControllerConnectedToTestDapp({ + useLocalhostHostname: true, + }) .withPreferencesController({ securityAlertsEnabled: true, }) @@ -109,7 +108,7 @@ describe('PPOM Blockaid Alert - Set Trade farming order @no-mmi', function () { async ({ driver }) => { await unlockWallet(driver); - await openDapp(driver); + await driver.openNewPage('http://localhost:8080'); const expectedTitle = 'This is a deceptive request'; const expectedDescription = @@ -117,27 +116,19 @@ describe('PPOM Blockaid Alert - Set Trade farming order @no-mmi', function () { // Click TestDapp button to send JSON-RPC request await driver.clickElement('#maliciousTradeOrder'); - - // Wait for confirmation pop-up - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.assertElementNotPresent('.loading-indicator'); - const bannerAlertFoundByTitle = await driver.findElement({ - css: bannerAlertSelector, + await driver.waitForSelector({ + css: '.mm-text--body-lg-medium', text: expectedTitle, }); - const bannerAlertText = await bannerAlertFoundByTitle.getText(); - assert( - bannerAlertFoundByTitle, - `Banner alert not found. Expected Title: ${expectedTitle} \nExpected reason: approval_farming\n`, - ); - assert( - bannerAlertText.includes(expectedDescription), - `Unexpected banner alert description. Expected: ${expectedDescription} \nExpected reason: approval_farming\n`, - ); + await driver.waitForSelector({ + css: '.mm-text--body-md', + text: expectedDescription, + }); }, ); }); diff --git a/test/e2e/tests/privacy-mode/privacy-mode.spec.js b/test/e2e/tests/privacy-mode/privacy-mode.spec.js new file mode 100644 index 000000000000..a4d2c2245752 --- /dev/null +++ b/test/e2e/tests/privacy-mode/privacy-mode.spec.js @@ -0,0 +1,106 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + unlockWallet, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Privacy Mode', function () { + it('should activate privacy mode, then deactivate it', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().withPreferencesController().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + async function checkForHeaderValue(value) { + const balanceElement = await driver.findElement( + '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + ); + const surveyText = await balanceElement.getText(); + assert.equal( + surveyText, + value, + `Header balance should be "${value}"`, + ); + } + + async function checkForTokenValue(value) { + const balanceElement = await driver.findElement( + '[data-testid="multichain-token-list-item-secondary-value"]', + ); + const surveyText = await balanceElement.getText(); + assert.equal(surveyText, value, `Token balance should be "${value}"`); + } + + async function checkForPrivacy() { + await checkForHeaderValue('••••••'); + await checkForTokenValue('•••••••••'); + } + + async function checkForNoPrivacy() { + await checkForHeaderValue('25'); + await checkForTokenValue('25 ETH'); + } + + async function togglePrivacy() { + const balanceElement = await driver.findElement( + '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + ); + const initialText = await balanceElement.getText(); + + await driver.clickElement('[data-testid="sensitive-toggle"]'); + await driver.wait(async () => { + const currentText = await balanceElement.getText(); + return currentText !== initialText; + }, 2e3); + } + + await unlockWallet(driver); + await checkForNoPrivacy(); + await togglePrivacy(); + await checkForPrivacy(); + await togglePrivacy(); + await checkForNoPrivacy(); + }, + ); + }); + + it('should hide fiat balance and token balance when privacy mode is activated', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().withPreferencesController().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + async function togglePrivacy() { + const balanceElement = await driver.findElement( + '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + ); + const initialText = await balanceElement.getText(); + + await driver.clickElement('[data-testid="sensitive-toggle"]'); + await driver.wait(async () => { + const currentText = await balanceElement.getText(); + return currentText !== initialText; + }, 2e3); + } + + await togglePrivacy(); + await driver.clickElement('[data-testid="account-menu-icon"]'); + const valueText = await driver.findElement( + '[data-testid="account-value-and-suffix"]', + ); + const valueTextContent = await valueText.getText(); + + assert.equal(valueTextContent, '••••••'); + }, + ); + }); +}); diff --git a/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts b/test/e2e/tests/privacy/account-tracker-api-usage.spec.ts similarity index 93% rename from test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts rename to test/e2e/tests/privacy/account-tracker-api-usage.spec.ts index f0cd40cb7373..24f2318fa13b 100644 --- a/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts +++ b/test/e2e/tests/privacy/account-tracker-api-usage.spec.ts @@ -4,11 +4,12 @@ import { MockedEndpoint } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { defaultGanacheOptions, - unlockWallet, veryLargeDelayMs, withFixtures, } from '../../helpers'; import { Mockttp } from '../../mock-e2e'; +import HomePage from '../../page-objects/pages/homepage'; +import { loginWithoutBalanceValidation } from '../../page-objects/flows/login.flow'; async function mockInfura(mockServer: Mockttp): Promise { const blockNumber = { value: 0 }; @@ -122,7 +123,9 @@ describe('Account Tracker API Usage', function () { )} request has been made to infura before opening the UI`, ); - await unlockWallet(driver); + await loginWithoutBalanceValidation(driver); + const homepage = new HomePage(driver); + await homepage.check_pageIsLoaded(); await driver.delay(veryLargeDelayMs); allInfuraJsonRpcRequests = await getAllInfuraJsonRpcRequests( @@ -158,7 +161,9 @@ describe('Account Tracker API Usage', function () { testSpecificMock: mockInfura, }, async ({ driver, mockedEndpoint }) => { - await unlockWallet(driver); + await loginWithoutBalanceValidation(driver); + const homepage = new HomePage(driver); + await homepage.check_pageIsLoaded(); await driver.delay(veryLargeDelayMs); const initialInfuraJsonRpcRequests = await getAllInfuraJsonRpcRequests( mockedEndpoint, diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index 674ba8772e29..e6439c569339 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -1,13 +1,10 @@ const { strict: assert } = require('assert'); -const { - TEST_SEED_PHRASE, - withFixtures, - importSRPOnboardingFlow, - WALLET_PASSWORD, - defaultGanacheOptions, -} = require('../../helpers'); +const { defaultGanacheOptions, withFixtures } = require('../../helpers'); const { METAMASK_STALELIST_URL } = require('../phishing-controller/helpers'); const FixtureBuilder = require('../../fixture-builder'); +const { + importSRPOnboardingFlow, +} = require('../../page-objects/flows/onboarding.flow'); async function mockApis(mockServer) { return [ @@ -26,8 +23,8 @@ async function mockApis(mockServer) { }; }), await mockServer - .forGet('https://min-api.cryptocompare.com/data/price') - .withQuery({ fsym: 'ETH', tsyms: 'USD' }) + .forGet('https://min-api.cryptocompare.com/data/pricemulti') + .withQuery({ fsyms: 'ETH', tsyms: 'usd' }) .thenCallback(() => { return { statusCode: 200, @@ -49,12 +46,7 @@ describe('MetaMask onboarding @no-mmi', function () { testSpecificMock: mockApis, }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); + await importSRPOnboardingFlow({ driver }); await driver.clickElement({ text: 'Manage default privacy settings', @@ -134,12 +126,7 @@ describe('MetaMask onboarding @no-mmi', function () { testSpecificMock: mockApis, }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); + await importSRPOnboardingFlow({ driver }); await driver.clickElement({ text: 'Manage default privacy settings', @@ -171,6 +158,8 @@ describe('MetaMask onboarding @no-mmi', function () { // 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"]'); + // intended delay to allow for network requests to complete + await driver.delay(1000); for (let i = 0; i < mockedEndpoints.length; i += 1) { const requests = await mockedEndpoints[i].getSeenRequests(); assert.equal( diff --git a/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts b/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts new file mode 100644 index 000000000000..1644cb068a3a --- /dev/null +++ b/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts @@ -0,0 +1,188 @@ +import assert from 'assert'; +import { Mockttp, MockedEndpoint } from 'mockttp'; +import { withFixtures, regularDelayMs } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import HomePage from '../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page'; +import { + importSRPOnboardingFlow, + createNewWalletOnboardingFlow, +} from '../../page-objects/flows/onboarding.flow'; + +// Mock function implementation for Infura requests +async function mockInfura(mockServer: Mockttp): Promise { + const infuraUrl = + 'https://mainnet.infura.io/v3/00000000000000000000000000000000'; + const sampleAddress = '1111111111111111111111111111111111111111'; + return [ + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_blockNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x1', + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBalance' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x0', + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: {}, + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_call' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: `0x000000000000000000000000${sampleAddress}`, + }, + }; + }), + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'net_version' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { id: 8262367391254633, jsonrpc: '2.0', result: '1337' }, + }; + }), + ]; +} + +describe('MetaMask onboarding @no-mmi', function () { + it("doesn't make any network requests to infura before create new wallet onboarding is completed", async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }) + .withNetworkControllerOnMainnet() + .build(), + title: this.test?.fullTitle(), + testSpecificMock: mockInfura, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + await createNewWalletOnboardingFlow(driver); + + // Check no requests are made before completing creat new wallet onboarding + // Intended delay to ensure we cover at least 1 polling loop of time for the network request + await driver.delay(regularDelayMs); + for (const mockedEndpoint of mockedEndpoints) { + const isPending = await mockedEndpoint.isPending(); + assert.equal( + isPending, + true, + `${mockedEndpoint} mock should still be pending before onboarding`, + ); + const requests = await mockedEndpoint.getSeenRequests(); + assert.equal( + requests.length, + 0, + `${mockedEndpoint} should make no requests before onboarding`, + ); + } + + // complete create new wallet onboarding + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // network requests happen here + for (const mockedEndpoint of mockedEndpoints) { + await driver.wait(async () => { + const isPending = await mockedEndpoint.isPending(); + return isPending === false; + }, driver.timeout); + + const requests = await mockedEndpoint.getSeenRequests(); + assert.equal( + requests.length > 0, + true, + `${mockedEndpoint} should make requests after onboarding`, + ); + } + }, + ); + }); + + it("doesn't make any network requests to infura before onboarding by import is completed", async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }) + .withNetworkControllerOnMainnet() + .build(), + title: this.test?.fullTitle(), + testSpecificMock: mockInfura, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + await importSRPOnboardingFlow({ driver }); + + // Check no requests before completing onboarding + // Intended delay to ensure we cover at least 1 polling loop of time for the network request + await driver.delay(regularDelayMs); + for (const mockedEndpoint of mockedEndpoints) { + const requests = await mockedEndpoint.getSeenRequests(); + assert.equal( + requests.length, + 0, + `${mockedEndpoint} should make no requests before import wallet onboarding complete`, + ); + } + + // complete import wallet onboarding + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // requests happen here + for (const mockedEndpoint of mockedEndpoints) { + await driver.wait(async () => { + const isPending = await mockedEndpoint.isPending(); + return isPending === false; + }, driver.timeout); + + const requests = await mockedEndpoint.getSeenRequests(); + assert.equal( + requests.length > 0, + true, + `${mockedEndpoint} should make requests after onboarding`, + ); + } + }, + ); + }); +}); 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 bd52558ec67f..c30d6a73c063 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 @@ -1,4 +1,4 @@ -const { strict: assert } = require('assert'); +const { By } = require('selenium-webdriver'); const FixtureBuilder = require('../../fixture-builder'); const { withFixtures, @@ -10,7 +10,6 @@ const { WINDOW_TITLES, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -59,7 +58,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.delay(regularDelayMs); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', @@ -98,7 +97,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.delay(regularDelayMs); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', @@ -134,16 +133,12 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - 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"]', @@ -162,14 +157,10 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.delay(largeDelayMs); 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"]', 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 d52d45701563..5814d8a60a2b 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 @@ -9,6 +9,7 @@ const { defaultGanacheOptions, tempToggleSettingRedesignedConfirmations, WINDOW_TITLES, + largeDelayMs, } = require('../../helpers'); describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { @@ -90,7 +91,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); - await driver.findElement({ + await driver.waitForSelector({ css: '[id="chainId"]', text: '0x53a', }); @@ -111,7 +112,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); - await driver.findElement({ + await driver.waitForSelector({ css: '[id="chainId"]', text: '0x3e8', }); @@ -132,21 +133,24 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the send confirmation. - await driver.findElement({ + await driver.waitForSelector({ css: '[data-testid="network-display"]', text: 'Localhost 7777', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.delay(largeDelayMs); await driver.waitUntilXWindowHandles(4); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the signTypedData confirmation. - await driver.findElement({ + await driver.waitForSelector({ css: '[data-testid="signature-request-network-display"]', text: 'Localhost 8546', }); + + await driver.clickElement({ text: 'Reject', tag: 'button' }); }, ); }); 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 1c1baa17fb5a..64ac781a20e0 100644 --- a/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js @@ -7,7 +7,6 @@ const { DAPP_URL, regularDelayMs, WINDOW_TITLES, - switchToNotificationWindow, defaultGanacheOptions, } = require('../../helpers'); @@ -45,19 +44,15 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { // Create Token await driver.clickElement({ text: 'Create Token', tag: 'button' }); - await switchToNotificationWindow(driver); - await driver.findClickableElement({ text: 'Confirm', tag: 'button' }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Confirm', tag: 'button' }); // Wait for token address to populate in dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.wait(async () => { - const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', - ); - const tokenAddresses = await tokenAddressesElement.getText(); - return tokenAddresses !== ''; - }, 10000); + await driver.waitForSelector({ + css: '#tokenAddresses', + text: '0x581c3C1A2A4EBDE2A0Df29B5cf4c116E42945947', + }); // Watch Asset 1st call await driver.clickElement({ @@ -65,11 +60,9 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { tag: 'button', }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // Switch Ethereum Chain - await driver.findClickableElement('#switchEthereumChain'); await driver.clickElement('#switchEthereumChain'); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -83,7 +76,7 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { // Wait for token to show in list of tokens to watch await driver.delay(regularDelayMs); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); const multipleSuggestedtokens = await driver.findElements( '.confirm-add-suggested-token__token-list-item', @@ -92,7 +85,7 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { // Confirm only 1 token is present in suggested token list assert.equal(multipleSuggestedtokens.length, 1); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); 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 446d579630bf..958854a5252c 100644 --- a/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js @@ -72,10 +72,10 @@ describe('MetaMask Responsive UI', function () { await driver.clickElement('[data-testid="pin-extension-done"]'); await driver.assertElementNotPresent('.loading-overlay__spinner'); // assert balance - const balance = await driver.findElement( - '[data-testid="eth-overview__primary-currency"]', - ); - assert.ok(/^0\sETH$/u.test(await balance.getText())); + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"]', + text: '0', + }); }, ); }); @@ -93,11 +93,14 @@ describe('MetaMask Responsive UI', function () { await driver.navigate(); // Import Secret Recovery Phrase - const restoreSeedLink = await driver.findClickableElement( - '.unlock-page__link', - ); - assert.equal(await restoreSeedLink.getText(), 'Forgot password?'); - await restoreSeedLink.click(); + await driver.waitForSelector({ + tag: 'span', + text: 'Localhost 8545', + }); + await driver.clickElement({ + css: '.unlock-page__link', + text: 'Forgot password?', + }); await driver.pasteIntoField( '[data-testid="import-srp__srp-word-0"]', diff --git a/test/e2e/tests/settings/about-metamask-ui-validation.spec.ts b/test/e2e/tests/settings/about-metamask-ui-validation.spec.ts index abc3c4857957..ed2702fef413 100644 --- a/test/e2e/tests/settings/about-metamask-ui-validation.spec.ts +++ b/test/e2e/tests/settings/about-metamask-ui-validation.spec.ts @@ -68,16 +68,11 @@ describe('Setting - About MetaMask : @no-mmi', function (this: Suite) { ); // verify the version number of the MetaMask - const metaMaskVersion = await driver.findElement( - selectors.metaMaskVersion, - ); - const getVersionNumber = await metaMaskVersion.getText(); const { version } = packageJson; - assert.equal( - getVersionNumber, - version, - 'Meta Mask version is incorrect in the about view section', - ); + await driver.waitForSelector({ + css: selectors.metaMaskVersion, + text: version, + }); // Validating the header text const isHeaderTextPresent = await driver.isElementPresent( diff --git a/test/e2e/tests/settings/address-book.spec.js b/test/e2e/tests/settings/address-book.spec.js index e81bd7c544aa..b71f45c2cf21 100644 --- a/test/e2e/tests/settings/address-book.spec.js +++ b/test/e2e/tests/settings/address-book.spec.js @@ -39,12 +39,11 @@ describe('Address Book', function () { await driver.clickElement({ css: 'button', text: 'Contacts' }); - const recipientTitle = await driver.findElement( - '.address-list-item__label', - ); + await driver.waitForSelector({ + css: '.address-list-item__label', + text: 'Test Name 1', + }); - const recipientRowTitleString = await recipientTitle.getText(); - assert.equal(recipientRowTitleString, 'Test Name 1'); await driver.clickElement('.address-list-item__label'); await driver.fill('input[placeholder="0"]', '2'); @@ -70,6 +69,44 @@ describe('Address Book', function () { }, ); }); + + it('Adds a new contact to the address book', 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.clickElement({ text: 'Contacts', tag: 'div' }); + + await driver.clickElement('.address-book__link'); + + await driver.fill('#nickname', 'Test User'); + + await driver.fill( + '[data-testid="ens-input"]', + '0x56A355d3427bC2B1E22c78197AF091230919Cc2A', + ); + + await driver.clickElement('[data-testid="page-container-footer-next"]'); + + await driver.waitForSelector({ + text: 'Test User', + css: '.address-list-item__label', + }); + await driver.waitForSelector({ + css: '[data-testid="address-list-item-address"]', + text: '0x56A35...9Cc2A', + }); + }, + ); + }); + it('Edit entry in address book', async function () { await withFixtures( { @@ -111,25 +148,15 @@ describe('Address Book', function () { await driver.clickElement('[data-testid="page-container-footer-next"]'); - const recipientUsername = await driver.findElement({ + await driver.waitForSelector({ text: 'Test Name Edit', css: '.address-list-item__label', }); - assert.equal( - await recipientUsername.getText(), - 'Test Name Edit', - 'Username is not edited correctly', - ); - - const recipientAddress = await driver.findElement( - '[data-testid="address-list-item-address"]', - ); - assert.equal( - await recipientAddress.getText(), - shortenAddress('0x74cE91B75935D6Bedc27eE002DeFa566c5946f74'), - 'Recipient address is not edited correctly', - ); + await driver.waitForSelector({ + css: '[data-testid="address-list-item-address"]', + text: shortenAddress('0x74cE91B75935D6Bedc27eE002DeFa566c5946f74'), + }); }, ); }); diff --git a/test/e2e/tests/settings/auto-lock.spec.js b/test/e2e/tests/settings/auto-lock.spec.js index 7d7a159d4a1b..46021ed0bb46 100644 --- a/test/e2e/tests/settings/auto-lock.spec.js +++ b/test/e2e/tests/settings/auto-lock.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, openMenuSafe, @@ -41,12 +40,11 @@ describe('Auto-Lock Timer', function () { '[data-testid="advanced-setting-auto-lock"] button', ); // Verify the wallet is locked - const pageTitle = await driver.findElement( - '[data-testid="unlock-page-title"]', - ); - const unlockButton = await driver.findElement('.unlock-page button'); - assert.equal(await pageTitle.getText(), 'Welcome back!'); - assert.equal(await unlockButton.isDisplayed(), true); + await driver.waitForSelector({ + css: '[data-testid="unlock-page-title"]', + text: 'Welcome back!', + }); + await driver.waitForSelector('.unlock-page button'); }, ); }); diff --git a/test/e2e/tests/settings/localization.spec.js b/test/e2e/tests/settings/localization.spec.js index 57dbfd5f68cf..229c385efbeb 100644 --- a/test/e2e/tests/settings/localization.spec.js +++ b/test/e2e/tests/settings/localization.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -6,6 +5,23 @@ const { } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); +async function mockPhpConversion(mockServer) { + return await mockServer + .forGet('https://min-api.cryptocompare.com/data/pricemulti') + .withQuery({ fsyms: 'ETH', tsyms: 'php,USD' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + ETH: { + PHP: '100000', + USD: '2500', + }, + }, + }; + }); +} + describe('Localization', function () { it('can correctly display Philippine peso symbol and code', async function () { await withFixtures( @@ -22,18 +38,22 @@ describe('Localization', function () { }) .build(), ganacheOptions: defaultGanacheOptions, + testSpecificMock: mockPhpConversion, title: this.test.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); // 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 balanceText = await primaryBalance.getText(); - assert.ok(balanceText.startsWith('₱')); - assert.ok(balanceText.endsWith('PHP')); + await driver.waitForSelector({ + tag: 'span', + text: 'PHP', + }); + + await driver.waitForSelector({ + tag: 'span', + text: '₱2,500,000.00', + }); }, ); }); diff --git a/test/e2e/tests/settings/settings-general.spec.js b/test/e2e/tests/settings/settings-general.spec.js index 5e75c857a7f8..ef07a53d01f2 100644 --- a/test/e2e/tests/settings/settings-general.spec.js +++ b/test/e2e/tests/settings/settings-general.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, openMenuSafe, @@ -28,25 +27,15 @@ describe('Settings', function () { '[data-testid="jazz_icon"] .settings-page__content-item__identicon__item__icon--active', ); - const jazziconText = await driver.findElement({ + await driver.waitForSelector({ tag: 'h6', text: 'Jazzicons', }); - assert.equal( - await jazziconText.getText(), - 'Jazzicons', - 'Text for icon should be Jazzicons', - ); - const blockiesText = await driver.findElement({ + await driver.waitForSelector({ tag: 'h6', text: 'Blockies', }); - assert.equal( - await blockiesText.getText(), - 'Blockies', - 'Text for icon should be Blockies', - ); }, ); }); diff --git a/test/e2e/tests/settings/settings-security-reveal-srp.spec.js b/test/e2e/tests/settings/settings-security-reveal-srp.spec.js index 7cbdaefe73ed..7f68f53f8dc7 100644 --- a/test/e2e/tests/settings/settings-security-reveal-srp.spec.js +++ b/test/e2e/tests/settings/settings-security-reveal-srp.spec.js @@ -52,10 +52,10 @@ describe('Reveal SRP through settings', function () { await tapAndHoldToRevealSRP(driver); // confirm SRP text matches expected - const displayedSRP = await driver.findVisibleElement( - '[data-testid="srp_text"]', - ); - assert.equal(await displayedSRP.getText(), E2E_SRP); + await driver.waitForSelector({ + css: '[data-testid="srp_text"]', + text: E2E_SRP, + }); // copy SRP text to clipboard await driver.clickElement({ diff --git a/test/e2e/tests/settings/show-hex-data.spec.js b/test/e2e/tests/settings/show-hex-data.spec.js index 5e0ae9a133a0..4bef79ca0a3b 100644 --- a/test/e2e/tests/settings/show-hex-data.spec.js +++ b/test/e2e/tests/settings/show-hex-data.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -84,15 +83,10 @@ describe('Check the toggle for hex data', function () { await sendTransactionAndVerifyHexData(driver); // Verify hex data in the container content - const pageContentContainer = await driver.findElement( - selectors.containerContent, - ); - const pageContentContainerText = await pageContentContainer.getText(); - assert.equal( - pageContentContainerText.includes(inputData.hexDataText), - true, - 'Hex data is incorrect', - ); + await driver.waitForSelector({ + tag: 'p', + text: '0x0abc', + }); }, ); }); diff --git a/test/e2e/tests/simulation-details/simulation-details.spec.ts b/test/e2e/tests/simulation-details/simulation-details.spec.ts index 46f4cdb5860a..bbac41b5a1a1 100644 --- a/test/e2e/tests/simulation-details/simulation-details.spec.ts +++ b/test/e2e/tests/simulation-details/simulation-details.spec.ts @@ -215,7 +215,7 @@ describe('Simulation Details', () => { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ css: '[data-testid="simulation-details-layout"]', - text: 'No changes predicted for your wallet', + text: 'No changes', }); }, ); @@ -276,7 +276,7 @@ describe('Simulation Details', () => { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ css: '[data-testid="simulation-details-layout"]', - text: 'There was an error loading your estimation', + text: 'Unavailable', }); }, ); diff --git a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js index 917e289f320f..8e4254fb8b34 100644 --- a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js +++ b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js @@ -7,7 +7,7 @@ const dappPort = 8080; describe('The provider', function () { it('can be injected synchronously and successfully used by a dapp', async function () { - const dappServer = createStaticServer(__dirname); + const dappServer = createStaticServer({ public: __dirname }); dappServer.listen(dappPort); await new Promise((resolve, reject) => { dappServer.on('listening', resolve); diff --git a/test/e2e/tests/tokens/add-multiple-tokens.spec.js b/test/e2e/tests/tokens/add-multiple-tokens.spec.js index 490460e770cb..8be2a430b622 100644 --- a/test/e2e/tests/tokens/add-multiple-tokens.spec.js +++ b/test/e2e/tests/tokens/add-multiple-tokens.spec.js @@ -28,6 +28,11 @@ describe('Multiple ERC20 Watch Asset', function () { await openDapp(driver, undefined, DAPP_URL); // Create Token 1 + const createToken = await driver.findElement({ + text: 'Create Token', + tag: 'button', + }); + await driver.scrollToElement(createToken); await driver.clickElement({ text: 'Create Token', tag: 'button' }); await switchToNotificationWindow(driver); await driver.findClickableElement({ text: 'Confirm', tag: 'button' }); @@ -37,7 +42,7 @@ describe('Multiple ERC20 Watch Asset', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.wait(async () => { const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', + '#erc20TokenAddresses', ); const tokenAddresses = await tokenAddressesElement.getText(); return tokenAddresses !== ''; @@ -53,7 +58,7 @@ describe('Multiple ERC20 Watch Asset', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.wait(async () => { const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', + '#erc20TokenAddresses', ); const tokenAddresses = await tokenAddressesElement.getText(); return tokenAddresses.split(',').length === 2; @@ -69,7 +74,7 @@ describe('Multiple ERC20 Watch Asset', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.wait(async () => { const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', + '#erc20TokenAddresses', ); const tokenAddresses = await tokenAddressesElement.getText(); return tokenAddresses.split(',').length === 3; diff --git a/test/e2e/tests/transaction/edit-gas-fee.spec.js b/test/e2e/tests/transaction/edit-gas-fee.spec.js index 85ae4da3a31f..918831f8f3ad 100644 --- a/test/e2e/tests/transaction/edit-gas-fee.spec.js +++ b/test/e2e/tests/transaction/edit-gas-fee.spec.js @@ -1,11 +1,11 @@ const { strict: assert } = require('assert'); const { createInternalTransaction, + createDappTransaction, } = require('../../page-objects/flows/transaction'); const { withFixtures, - openDapp, unlockWallet, generateGanacheOptions, WINDOW_TITLES, @@ -172,11 +172,9 @@ describe('Editing Confirm Transaction', function () { // login to extension await unlockWallet(driver); - // open dapp and connect - await openDapp(driver); - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', + await createDappTransaction(driver, { + maxFeePerGas: '0x2000000000', + maxPriorityFeePerGas: '0x1000000000', }); // check transaction in extension popup @@ -198,12 +196,12 @@ describe('Editing Confirm Transaction', function () { '.currency-display-component__text', ); const transactionAmount = transactionAmounts[0]; - assert.equal(await transactionAmount.getText(), '0'); + assert.equal(await transactionAmount.getText(), '0.001'); // has correct updated value on the confirm screen the transaction await driver.waitForSelector({ css: '.currency-display-component__text', - text: '0.00021', + text: '0.00185144', }); // confirms the transaction @@ -227,7 +225,7 @@ describe('Editing Confirm Transaction', function () { '[data-testid="transaction-list-item-primary-currency"]', ); assert.equal(txValues.length, 1); - assert.ok(/-0\s*ETH/u.test(await txValues[0].getText())); + assert.ok(/-0.001\s*ETH/u.test(await txValues[0].getText())); }, ); }); diff --git a/test/e2e/tests/transaction/ens.spec.ts b/test/e2e/tests/transaction/ens.spec.ts index 4700ab8fac3f..47bae3e5e4cc 100644 --- a/test/e2e/tests/transaction/ens.spec.ts +++ b/test/e2e/tests/transaction/ens.spec.ts @@ -112,6 +112,7 @@ describe('ENS', function (this: Suite) { // click send button on homepage to start send flow const homepage = new HomePage(driver); + await homepage.check_pageIsLoaded(); await homepage.check_expectedBalanceIsDisplayed('<0.000001'); await homepage.startSendFlow(); diff --git a/test/e2e/tests/transaction/multiple-transactions.spec.js b/test/e2e/tests/transaction/multiple-transactions.spec.js index 8f1318c31e3b..4d913cb07edb 100644 --- a/test/e2e/tests/transaction/multiple-transactions.spec.js +++ b/test/e2e/tests/transaction/multiple-transactions.spec.js @@ -26,19 +26,13 @@ describe('Multiple transactions', function () { // initiates a transaction from the dapp await openDapp(driver); // creates first transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // creates second transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirms second transaction @@ -94,19 +88,13 @@ describe('Multiple transactions', function () { // initiates a transaction from the dapp await openDapp(driver); // creates first transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // creates second transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // rejects second transaction @@ -141,3 +129,10 @@ describe('Multiple transactions', function () { ); }); }); + +async function createDappTransaction(driver) { + await driver.clickElement({ + text: 'Send EIP 1559 Without Gas', + tag: 'button', + }); +} diff --git a/test/e2e/tests/transaction/simple-send.spec.ts b/test/e2e/tests/transaction/simple-send.spec.ts index 0615a0e21d74..25f2368a9cfc 100644 --- a/test/e2e/tests/transaction/simple-send.spec.ts +++ b/test/e2e/tests/transaction/simple-send.spec.ts @@ -4,7 +4,7 @@ import { Ganache } from '../../seeder/ganache'; import { withFixtures, defaultGanacheOptions } from '../../helpers'; import FixtureBuilder from '../../fixture-builder'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; -import { sendTransaction } from '../../page-objects/flows/send-transaction.flow'; +import { sendTransactionToAddress } from '../../page-objects/flows/send-transaction.flow'; import HomePage from '../../page-objects/pages/homepage'; describe('Simple send eth', function (this: Suite) { @@ -23,13 +23,13 @@ describe('Simple send eth', function (this: Suite) { ganacheServer?: Ganache; }) => { await loginWithBalanceValidation(driver, ganacheServer); - await sendTransaction( + await sendTransactionToAddress({ driver, - '0x985c30949c92df7a0bd42e0f3e3d539ece98db24', - '1', - '0.000042', - '1.000042', - ); + recipientAddress: '0x985c30949c92df7a0bd42e0f3e3d539ece98db24', + amount: '1', + gasFee: '0.000042', + totalFee: '1.000042', + }); const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_confirmedTxNumberDisplayedInActivity(); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 813d00d5e0e8..1a10f7c0199d 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -184,8 +184,8 @@ class Driver { * * To target an element based on its attribute using a CSS selector, * use square brackets ([]) to specify the attribute name and its value. - * @example Example to locate the ‘Buy & Sell’ button using its unique attribute data-testid and its value on the overview screen - * await driver.findElement('[data-testid="eth-overview-buy"]'); + * @example Example to locate the ‘Buy & Sell’ button using its unique attribute testId and its value on the overview screen + * await driver.findElement({testId: 'eth-overview-buy'}); * * To locate an element by XPath locator strategy * @example Example to locate 'Confirm' button on the send transaction page @@ -204,6 +204,11 @@ class Driver { // xpath locator. return By.xpath(locator.xpath); } else if (locator.text) { + // If a testId prop was provided along with text, convert that to a css prop and continue + if (locator.testId) { + locator.css = `[data-testid="${locator.testId}"]`; + } + // Providing a text prop, and optionally a tag or css prop, will use // xpath to look for an element with the tag that has matching text. if (locator.css) { @@ -232,7 +237,12 @@ class Driver { const quoted = quoteXPathText(locator.text); // The tag prop is optional and further refines which elements match return By.xpath(`//${locator.tag ?? '*'}[contains(text(), ${quoted})]`); + } else if (locator.testId) { + // Providing a testId prop will use css to look for an element with the + // data-testid attribute that matches the testId provided. + return By.css(`[data-testid="${locator.testId}"]`); } + throw new Error( `The locator '${locator}' is not supported by the E2E test driver`, ); @@ -276,6 +286,12 @@ class Driver { await new Promise((resolve) => setTimeout(resolve, time)); } + async delayFirefox(time) { + if (process.env.SELENIUM_BROWSER === 'firefox') { + await new Promise((resolve) => setTimeout(resolve, time)); + } + } + /** * Function to wait for a specific condition to be met within a given timeout period, * with an option to catch and handle any errors that occur during the wait. diff --git a/test/env.js b/test/env.js index 2dacb1d888ba..268f01af0e3b 100644 --- a/test/env.js +++ b/test/env.js @@ -16,3 +16,4 @@ process.env.PUSH_NOTIFICATIONS_SERVICE_URL = process.env.PORTFOLIO_URL = 'https://portfolio.test'; process.env.METAMASK_VERSION = 'MOCK_VERSION'; process.env.ENABLE_CONFIRMATION_REDESIGN = 'true'; +process.env.TZ = 'UTC'; diff --git a/test/integration/config/setupAfter.js b/test/integration/config/setupAfter.js index 39eba1e429a5..ad9e49178094 100644 --- a/test/integration/config/setupAfter.js +++ b/test/integration/config/setupAfter.js @@ -1,2 +1,9 @@ // This file is for Jest-specific setup only and runs before our Jest tests. +import { jestPreviewConfigure } from 'jest-preview'; +import '../config/assets/index.css'; import '../../helpers/setup-after-helper'; + +// Should be path from root of your project +jestPreviewConfigure({ + publicFolder: 'test/integration/config/assets', // No need to configure if `publicFolder` is `public` +}); diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 8e9c979562f2..5ff87bf7c533 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -1,6 +1,7 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import nock from 'nock'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -42,6 +43,7 @@ const getMetaMaskStateWithUnapprovedPermitSign = (accountAddress: string) => { unapprovedTypedMessages: { [pendingPermitId]: { id: pendingPermitId, + chainId: CHAIN_IDS.SEPOLIA, status: 'unapproved', time: pendingPermitTime, type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 690446caa533..5a9c311c9abd 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,5 +1,6 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -34,6 +35,7 @@ const getMetaMaskStateWithUnapprovedPersonalSign = (accountAddress: string) => { unapprovedPersonalMsgs: { [pendingPersonalSignId]: { id: pendingPersonalSignId, + chainId: CHAIN_IDS.SEPOLIA, status: 'unapproved', time: pendingPersonalSignTime, type: MESSAGE_TYPE.PERSONAL_SIGN, diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx index c2625e06e3e7..67862e6b9550 100644 --- a/test/integration/confirmations/transactions/contract-deployment.test.tsx +++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx @@ -284,9 +284,6 @@ describe('Contract Deployment Confirmation', () => { 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'), ); @@ -372,7 +369,6 @@ describe('Contract Deployment Confirmation', () => { 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(); diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx index b77e48f1d660..9a955e1a45fb 100644 --- a/test/integration/confirmations/transactions/contract-interaction.test.tsx +++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx @@ -302,9 +302,6 @@ describe('Contract Interaction Confirmation', () => { 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'), ); @@ -403,7 +400,6 @@ describe('Contract Interaction Confirmation', () => { 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(); diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 2d9e50002a18..a0ae3a8fb146 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -781,9 +781,10 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": true, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "petnamesEnabled": false, - "showConfirmationAdvancedDetails": false + "showConfirmationAdvancedDetails": false, + "showMultiRpcModal": false }, "preventPollingOnNetworkRestart": true, "previousAppVersion": "11.14.4", diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index e651e9c2ce29..b2c19536a138 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -223,11 +223,12 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": null, + "smartTransactionsOptInStatus": true, "hideZeroBalanceTokens": false, "petnamesEnabled": true, "redesignedConfirmationsEnabled": true, - "featureNotificationsEnabled": false + "featureNotificationsEnabled": false, + "privacyMode": false }, "preventPollingOnNetworkRestart": false, "previousAppVersion": "", diff --git a/test/integration/notifications&auth/data/notification-state.ts b/test/integration/notifications&auth/data/notification-state.ts new file mode 100644 index 000000000000..c58bf707f521 --- /dev/null +++ b/test/integration/notifications&auth/data/notification-state.ts @@ -0,0 +1,54 @@ +import { + INotification, + TRIGGER_TYPES, + processNotification, +} from '@metamask/notification-services-controller/notification-services'; +import { + createMockNotificationEthSent, + createMockFeatureAnnouncementRaw, +} from '@metamask/notification-services-controller/notification-services/mocks'; +import mockMetaMaskState from '../../data/integration-init-state.json'; + +const notificationsAccountAddress = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ].address; + +export const ethSentNotification = processNotification( + createMockNotificationEthSent(), +) as Extract; + +if (ethSentNotification.type === TRIGGER_TYPES.ETH_SENT) { + ethSentNotification.address = notificationsAccountAddress; + ethSentNotification.data.from = notificationsAccountAddress; + ethSentNotification.isRead = true; +} + +export const featureNotification = processNotification( + createMockFeatureAnnouncementRaw(), +) as Extract; + +if (featureNotification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT) { + featureNotification.isRead = true; +} + +export const getMockedNotificationsState = () => { + return { + ...mockMetaMaskState, + isProfileSyncingEnabled: true, + isProfileSyncingUpdateLoading: false, + isMetamaskNotificationsFeatureSeen: true, + isNotificationServicesEnabled: true, + isFeatureAnnouncementsEnabled: true, + notifications: {}, + metamaskNotificationsReadList: [featureNotification.id], + metamaskNotificationsList: [featureNotification, ethSentNotification], + isUpdatingMetamaskNotifications: false, + isFetchingMetamaskNotifications: false, + isUpdatingMetamaskNotificationsAccount: [], + useExternalServices: true, + pendingApprovalCount: 0, + pendingApprovals: {}, + }; +}; diff --git a/test/integration/notifications&auth/notifications-activation.test.tsx b/test/integration/notifications&auth/notifications-activation.test.tsx new file mode 100644 index 000000000000..e11e58dad320 --- /dev/null +++ b/test/integration/notifications&auth/notifications-activation.test.tsx @@ -0,0 +1,196 @@ +import { + act, + fireEvent, + waitFor, + screen, + within, +} from '@testing-library/react'; +import { integrationTestRender } from '../../lib/render-helpers'; +import * as backgroundConnection from '../../../ui/store/background-connection'; +import { createMockImplementation } from '../helpers'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; +import { getMockedNotificationsState } from './data/notification-state'; + +jest.mock('../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...(mockRequests ?? {}), + }), + ); +}; + +const trackNotificationsActivatedMetaMetricsEvent = async ( + actionType: string, + profileSyncEnabled: boolean, +) => { + const expectedCall = [ + 'trackMetaMetricsEvent', + [ + expect.objectContaining({ + event: MetaMetricsEventName.NotificationsActivated, + category: MetaMetricsEventCategory.NotificationsActivationFlow, + properties: { + action_type: actionType, + is_profile_syncing_enabled: profileSyncEnabled, + }, + }), + ], + ]; + + expect( + mockedBackgroundConnection.submitRequestToBackground.mock.calls, + ).toStrictEqual(expect.arrayContaining([expectedCall])); +}; +describe('Notifications Activation', () => { + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks(); + }); + + afterEach(() => { + window.history.pushState({}, '', '/'); // return to homescreen + }); + + const clickElement = async (testId: string) => { + await act(async () => { + fireEvent.click(screen.getByTestId(testId)); + }); + }; + + const waitForElement = async (testId: string) => { + await waitFor(() => { + expect(screen.getByTestId(testId)).toBeInTheDocument(); + }); + }; + + it('should successfully activate notification for the first time', async () => { + const mockedState = getMockedNotificationsState(); + await act(async () => { + await integrationTestRender({ + preloadedState: { + ...mockedState, + isProfileSyncingEnabled: false, + isNotificationServicesEnabled: false, + isFeatureAnnouncementsEnabled: false, + isMetamaskNotificationsFeatureSeen: false, + }, + backgroundConnection: backgroundConnectionMocked, + }); + + await clickElement('account-options-menu-button'); + await waitForElement('notifications-menu-item'); + await clickElement('notifications-menu-item'); + + await waitFor(() => { + expect( + within(screen.getByRole('dialog')).getByText('Turn on'), + ).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click(screen.getByText('Turn on')); + }); + + await waitFor(() => { + const createOnChainTriggersCall = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'createOnChainTriggers', + ); + + expect(createOnChainTriggersCall?.[0]).toBe('createOnChainTriggers'); + }); + + await trackNotificationsActivatedMetaMetricsEvent('started', false); + await trackNotificationsActivatedMetaMetricsEvent('activated', true); + }); + }); + + it('should successfully send correct metrics when notifications modal is dismissed', async () => { + const mockedState = getMockedNotificationsState(); + await act(async () => { + await integrationTestRender({ + preloadedState: { + ...mockedState, + isProfileSyncingEnabled: false, + isNotificationServicesEnabled: false, + isFeatureAnnouncementsEnabled: false, + isMetamaskNotificationsFeatureSeen: false, + }, + backgroundConnection: backgroundConnectionMocked, + }); + + await clickElement('account-options-menu-button'); + await waitForElement('notifications-menu-item'); + await clickElement('notifications-menu-item'); + + await waitFor(() => { + expect( + within(screen.getByRole('dialog')).getByText('Turn on'), + ).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'Close', + }), + ); + }); + + await trackNotificationsActivatedMetaMetricsEvent('dismissed', false); + }); + }); + + it('should successfully send correct metrics when notifications modal is dismissed', async () => { + const mockedState = getMockedNotificationsState(); + await act(async () => { + await integrationTestRender({ + preloadedState: { + ...mockedState, + isProfileSyncingEnabled: false, + isNotificationServicesEnabled: false, + isFeatureAnnouncementsEnabled: false, + isMetamaskNotificationsFeatureSeen: false, + }, + backgroundConnection: backgroundConnectionMocked, + }); + + await clickElement('account-options-menu-button'); + await waitForElement('notifications-menu-item'); + await clickElement('notifications-menu-item'); + + await waitFor(() => { + expect( + within(screen.getByRole('dialog')).getByText('Turn on'), + ).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'Close', + }), + ); + }); + + await trackNotificationsActivatedMetaMetricsEvent('dismissed', false); + }); + }); +}); diff --git a/test/integration/notifications&auth/notifications-list.test.tsx b/test/integration/notifications&auth/notifications-list.test.tsx new file mode 100644 index 000000000000..4e17a53db107 --- /dev/null +++ b/test/integration/notifications&auth/notifications-list.test.tsx @@ -0,0 +1,246 @@ +import { + act, + fireEvent, + waitFor, + within, + screen, +} from '@testing-library/react'; +import { integrationTestRender } from '../../lib/render-helpers'; +import * as backgroundConnection from '../../../ui/store/background-connection'; +import { createMockImplementation } from '../helpers'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; +import { + ethSentNotification, + featureNotification, + getMockedNotificationsState, +} from './data/notification-state'; + +jest.mock('../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...(mockRequests ?? {}), + }), + ); +}; + +const getStateWithTwoUnreadNotifications = () => { + const state = getMockedNotificationsState(); + return { + ...state, + metamaskNotificationsList: [ + { + ...state.metamaskNotificationsList[0], + isRead: false, + }, + { + ...state.metamaskNotificationsList[1], + isRead: false, + }, + ], + }; +}; + +describe('Notifications List', () => { + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks(); + }); + + afterEach(() => { + window.history.pushState({}, '', '/'); // return to homescreen + }); + + it('should show the correct number of unread notifications on the badge', async () => { + const mockedState = getStateWithTwoUnreadNotifications(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + await waitFor(() => { + const unreadCount = screen.getByTestId( + 'notifications-tag-counter__unread-dot', + ); + expect(unreadCount).toBeInTheDocument(); + expect(unreadCount).toHaveTextContent('2'); + }); + }); + + it('should render notifications list and show correct details', async () => { + const mockedState = getStateWithTwoUnreadNotifications(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + fireEvent.click(screen.getByTestId('account-options-menu-button')); + + await waitFor(() => { + expect(screen.getByTestId('notifications-menu-item')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('notifications-menu-item')); + }); + + await waitFor(() => { + const notificationsList = screen.getByTestId('notifications-list'); + expect(notificationsList).toBeInTheDocument(); + expect(notificationsList.childElementCount).toBe(3); + + // Feature notification details + expect( + within(notificationsList).getByText(featureNotification.data.title), + ).toBeInTheDocument(); + expect( + within(notificationsList).getByText( + featureNotification.data.shortDescription, + ), + ).toBeInTheDocument(); + + // Eth sent notification details + const sentToElement = within(notificationsList).getByText('Sent to'); + expect(sentToElement).toBeInTheDocument(); + + const addressElement = sentToElement.nextElementSibling; + expect(addressElement).toHaveTextContent('0x881D4...D300D'); + + // Read all button + expect( + within(notificationsList).getByTestId( + 'notifications-list-read-all-button', + ), + ).toBeInTheDocument(); + + const unreadDot = screen.getAllByTestId('unread-dot'); + expect(unreadDot).toHaveLength(2); + }); + + await waitFor(() => { + const notificationsInteractionsEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => + call[0] === 'trackMetaMetricsEvent' && + call[1]?.[0].category === + MetaMetricsEventCategory.NotificationInteraction, + ); + + expect(notificationsInteractionsEvent?.[0]).toBe('trackMetaMetricsEvent'); + const [metricsEvent] = notificationsInteractionsEvent?.[1] as unknown as [ + { + event: string; + category: string; + properties: Record; + }, + ]; + + expect(metricsEvent?.event).toBe( + MetaMetricsEventName.NotificationsMenuOpened, + ); + + expect(metricsEvent?.category).toBe( + MetaMetricsEventCategory.NotificationInteraction, + ); + + expect(metricsEvent.properties).toMatchObject({ + unread_count: 2, + read_count: 0, + }); + }); + }); + + it('should not see mark all as read button if there are no unread notifications', async () => { + const mockedState = getMockedNotificationsState(); // all notifications are read by default + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedState, + backgroundConnection: backgroundConnectionMocked, + }); + + fireEvent.click(screen.getByTestId('account-options-menu-button')); + + await waitFor(() => { + expect( + screen.getByTestId('notifications-menu-item'), + ).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('notifications-menu-item')); + }); + + await waitFor(() => { + const notificationsList = screen.getByTestId('notifications-list'); + expect(notificationsList).toBeInTheDocument(); + + expect(notificationsList.childElementCount).toBe(2); + + expect( + screen.queryByTestId('notifications-list-read-all-button'), + ).not.toBeInTheDocument(); + + expect(screen.queryAllByTestId('unread-dot')).toHaveLength(0); + }); + }); + }); + + it('should send request for marking notifications as read to the background with the correct params', async () => { + const mockedState = getStateWithTwoUnreadNotifications(); + await act(async () => { + await integrationTestRender({ + preloadedState: mockedState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + fireEvent.click(screen.getByTestId('account-options-menu-button')); + + await waitFor(() => { + expect(screen.getByTestId('notifications-menu-item')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('notifications-menu-item')); + }); + + fireEvent.click(screen.getByTestId('notifications-list-read-all-button')); + + await waitFor(() => { + const markAllAsReadEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'markMetamaskNotificationsAsRead', + ); + + expect(markAllAsReadEvent?.[0]).toBe('markMetamaskNotificationsAsRead'); + expect(markAllAsReadEvent?.[1]).toStrictEqual([ + [ + { + id: featureNotification.id, + type: featureNotification.type, + isRead: false, + }, + { + id: ethSentNotification.id, + type: ethSentNotification.type, + isRead: false, + }, + ], + ]); + }); + }); +}); diff --git a/test/integration/notifications&auth/notifications-toggle.test.tsx b/test/integration/notifications&auth/notifications-toggle.test.tsx new file mode 100644 index 000000000000..8133e4c4bc3d --- /dev/null +++ b/test/integration/notifications&auth/notifications-toggle.test.tsx @@ -0,0 +1,224 @@ +import { + act, + fireEvent, + waitFor, + within, + screen, +} from '@testing-library/react'; +import { integrationTestRender } from '../../lib/render-helpers'; +import * as backgroundConnection from '../../../ui/store/background-connection'; +import { createMockImplementation } from '../helpers'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; +import { getMockedNotificationsState } from './data/notification-state'; + +jest.mock('../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...(mockRequests ?? {}), + }), + ); +}; + +describe('Notifications Toggle', () => { + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks(); + }); + + afterEach(() => { + window.history.pushState({}, '', '/'); // return to homescreen + }); + + const clickElement = async (testId: string) => { + await act(async () => { + fireEvent.click(screen.getByTestId(testId)); + }); + }; + + const waitForElement = async (testId: string) => { + await waitFor(() => { + expect(screen.getByTestId(testId)).toBeInTheDocument(); + }); + }; + + it('disabling notifications from settings', async () => { + const mockedState = getMockedNotificationsState(); + await act(async () => { + await integrationTestRender({ + preloadedState: { ...mockedState }, + backgroundConnection: backgroundConnectionMocked, + }); + + await clickElement('account-options-menu-button'); + await waitForElement('notifications-menu-item'); + await clickElement('notifications-menu-item'); + await waitForElement('notifications-settings-button'); + await clickElement('notifications-settings-button'); + await waitForElement('notifications-settings-allow-notifications'); + + const toggleSection = screen.getByTestId( + 'notifications-settings-allow-notifications', + ); + + await act(async () => { + fireEvent.click(within(toggleSection).getByRole('checkbox')); + }); + + await waitFor(() => { + const disableNotificationsCall = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'disableMetamaskNotifications', + ); + + const fetchAndUpdateMetamaskNotificationsCall = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'fetchAndUpdateMetamaskNotifications', + ); + + expect(disableNotificationsCall?.[0]).toBe( + 'disableMetamaskNotifications', + ); + + expect(fetchAndUpdateMetamaskNotificationsCall?.[0]).toBe( + 'fetchAndUpdateMetamaskNotifications', + ); + }); + + await waitFor(() => { + const metametrics = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => + call[0] === 'trackMetaMetricsEvent' && + call[1]?.[0].category === + MetaMetricsEventCategory.NotificationSettings, + ); + + expect(metametrics?.[0]).toBe('trackMetaMetricsEvent'); + + const [metricsEvent] = metametrics?.[1] as unknown as [ + { + event: string; + category: string; + properties: Record; + }, + ]; + + expect(metricsEvent?.event).toBe( + MetaMetricsEventName.NotificationsSettingsUpdated, + ); + + expect(metricsEvent?.category).toBe( + MetaMetricsEventCategory.NotificationSettings, + ); + + expect(metricsEvent?.properties).toMatchObject({ + settings_type: 'notifications', + was_profile_syncing_on: true, + old_value: true, + new_value: false, + }); + }); + }); + }); + + it('enabling product announcments from settings', async () => { + const mockedState = getMockedNotificationsState(); + await act(async () => { + await integrationTestRender({ + preloadedState: { + ...mockedState, + isProfileSyncingEnabled: false, + isNotificationServicesEnabled: true, + isFeatureAnnouncementsEnabled: false, + isMetamaskNotificationsFeatureSeen: true, + }, + backgroundConnection: backgroundConnectionMocked, + }); + + await clickElement('account-options-menu-button'); + await waitForElement('notifications-menu-item'); + await clickElement('notifications-menu-item'); + await waitForElement('notifications-settings-button'); + await clickElement('notifications-settings-button'); + await waitForElement('notifications-settings-allow-notifications'); + + const allToggles = screen.getAllByTestId('test-toggle'); + + await act(async () => { + fireEvent.click(allToggles[1]); + }); + + await waitFor(() => { + const enableFeatureNotifications = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'setFeatureAnnouncementsEnabled', + ); + + const fetchAndUpdateMetamaskNotificationsCall = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'fetchAndUpdateMetamaskNotifications', + ); + + expect(enableFeatureNotifications?.[0]).toBe( + 'setFeatureAnnouncementsEnabled', + ); + expect(enableFeatureNotifications?.[1]).toEqual([true]); + + expect(fetchAndUpdateMetamaskNotificationsCall?.[0]).toBe( + 'fetchAndUpdateMetamaskNotifications', + ); + }); + + await waitFor(() => { + const metametrics = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => + call[0] === 'trackMetaMetricsEvent' && + call[1]?.[0].category === + MetaMetricsEventCategory.NotificationSettings, + ); + + expect(metametrics?.[0]).toBe('trackMetaMetricsEvent'); + + const [metricsEvent] = metametrics?.[1] as unknown as [ + { + event: string; + category: string; + properties: Record; + }, + ]; + + expect(metricsEvent?.event).toBe( + MetaMetricsEventName.NotificationsSettingsUpdated, + ); + + expect(metricsEvent?.category).toBe( + MetaMetricsEventCategory.NotificationSettings, + ); + + expect(metricsEvent?.properties).toMatchObject({ + settings_type: 'product_announcements', + old_value: false, + new_value: true, + }); + }); + }); + }); +}); diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 55ffa7f9ba1b..3fe524634075 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -3,6 +3,7 @@ import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../shared/constants/network'; import { KeyringType } from '../../shared/constants/keyring'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../stub/networks'; +import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge/constants'; export const createGetSmartTransactionFeesApiResponse = () => { return { @@ -726,6 +727,8 @@ export const createBridgeMockStore = ( destNetworkAllowlist: [], ...featureFlagOverrides, }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quoteRequest: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, ...bridgeStateOverrides, }, }, diff --git a/test/lib/render-helpers.js b/test/lib/render-helpers.js index fa54ac1fb4ec..574415f2f3c6 100644 --- a/test/lib/render-helpers.js +++ b/test/lib/render-helpers.js @@ -99,6 +99,27 @@ export function renderHookWithProvider(hook, state, pathname = '/', Container) { }; } +/** + * Renders a hook with a provider and optional container. + * + * @template {(...args: any) => any} Hook + * @template {Parameters} HookParams + * @template {ReturnType} HookReturn + * @template {import('@testing-library/react-hooks').RenderHookResult} RenderHookResult + * @template {import('history').History} History + * @param {Hook} hook - The hook to be rendered. + * @param [state] - The initial state for the store. + * @param [pathname] - The initial pathname for the history. + * @param [Container] - An optional container component. + * @returns {RenderHookResult & { history: History }} The result of the rendered hook and the history object. + */ +export const renderHookWithProviderTyped = ( + hook, + state, + pathname = '/', + Container, +) => renderHookWithProvider(hook, state, pathname, Container); + export function renderWithLocalization(component) { const Wrapper = ({ children }) => ( diff --git a/ui/__mocks__/useNftCollectionsMetadata.js b/ui/__mocks__/useNftCollectionsMetadata.js new file mode 100644 index 000000000000..fd99ff219364 --- /dev/null +++ b/ui/__mocks__/useNftCollectionsMetadata.js @@ -0,0 +1,14 @@ +module.exports = { + useNftCollectionsMetadata: () => { + return { + '0x1': { + '0xc0ffee254729296a45a3885639ac7e10f9d54979': { + name: 'Everything I Own', + image: + 'https://img.reservoir.tools/images/v2/mainnet/z9JRSpLYGu7%2BCZoKWtAuAN%2F%2FMfWcOGcwki5%2FxXYtCb4OfGsOPvxN1LZHZ5%2BcuQGwJciTvgr58ThRjooWLMWehc1nSTXtbfFJ1TNtL%2FeIjglkPKsEG%2Fbem0E%2B3yo7tAUqlZ1ou0SMzGOfq%2FG1BHwIpgHQ524PRAlaynVkDcp8y58kALOPTQSDN1tgaqkZD%2FZiNBEaYq6Bp9XH8Vm8tMXsaQ%3D%3D?width=250', + isSpam: false, + }, + }, + }; + }, +}; diff --git a/ui/__mocks__/webextension-polyfill.js b/ui/__mocks__/webextension-polyfill.js index 693368f15e0c..dbf15f9ea145 100644 --- a/ui/__mocks__/webextension-polyfill.js +++ b/ui/__mocks__/webextension-polyfill.js @@ -1,3 +1,15 @@ +const getManifest = () => ({ manifest_version: 3 }); + +// Polyfill chrome.runtime for environments that do not support it +// E.g. Storybook +global.chrome = { + ...global?.chrome, + runtime: { + ...global?.chrome?.runtime, + getManifest, + }, +}; + module.exports = { - runtime: { getManifest: () => ({ manifest_version: 3 }) }, + runtime: { getManifest }, }; diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 0995f4b52a4a..9eefd48028ac 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -25,6 +25,7 @@ @import 'snaps/snap-ui-input/index'; @import 'snaps/snap-ui-file-input/index'; @import 'snaps/snap-ui-selector/index'; +@import 'snaps/snap-ui-link/index'; @import 'snaps/snap-delineator/index'; @import 'snaps/snap-home-menu/index'; @import 'snaps/snap-list-item/index'; diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx index 696c3ca7c89f..de771976e677 100644 --- 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 @@ -1,4 +1,6 @@ import React, { useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { getCurrentNetwork, getPreferences } from '../../../../../selectors'; import { Box, ButtonBase, @@ -25,6 +27,7 @@ import { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_POPUP, } from '../../../../../../shared/constants/app'; +import NetworkFilter from '../network-filter'; type AssetListControlBarProps = { showTokensLinks?: boolean; @@ -32,55 +35,116 @@ type AssetListControlBarProps = { const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { const t = useI18nContext(); - const controlBarRef = useRef(null); // Create a ref - const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const popoverRef = useRef(null); + const currentNetwork = useSelector(getCurrentNetwork); + const { tokenNetworkFilter } = useSelector(getPreferences); + const [isTokenSortPopoverOpen, setIsTokenSortPopoverOpen] = useState(false); + const [isNetworkFilterPopoverOpen, setIsNetworkFilterPopoverOpen] = + useState(false); + + const allNetworksFilterShown = Object.keys(tokenNetworkFilter ?? {}).length; const windowType = getEnvironmentType(); const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && windowType !== ENVIRONMENT_TYPE_POPUP; - const handleOpenPopover = () => { - setIsPopoverOpen(!isPopoverOpen); + const toggleTokenSortPopover = () => { + setIsNetworkFilterPopoverOpen(false); + setIsTokenSortPopoverOpen(!isTokenSortPopoverOpen); + }; + + const toggleNetworkFilterPopover = () => { + setIsTokenSortPopoverOpen(false); + setIsNetworkFilterPopoverOpen(!isNetworkFilterPopoverOpen); }; const closePopover = () => { - setIsPopoverOpen(false); + setIsTokenSortPopoverOpen(false); + setIsNetworkFilterPopoverOpen(false); }; return ( - - {t('sortBy')} - - + {process.env.FILTER_TOKENS_TOGGLE && ( + + {allNetworksFilterShown + ? currentNetwork?.nickname ?? t('currentNetwork') + : t('allNetworks')} + + )} + + + {t('sortBy')} + + + + + + + + { '0xc42edfcc21ed14dda456aa0756c153f7985d8813', '0x0', ); - expect(queryByText('Fund your wallet')).toBeInTheDocument(); + expect(queryByText('Tips for using a wallet')).toBeInTheDocument(); }); it('does not show the ramp card when the account has a balance', () => { const { queryByText } = render(); - expect(queryByText('Fund your wallet')).not.toBeInTheDocument(); + expect(queryByText('Tips for using a wallet')).not.toBeInTheDocument(); }); }); diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 5cfeb6803875..4cbe529e3df2 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -164,7 +164,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { setShowFundingMethodModal(false)} - title={t('selectFundingMethod')} + title={t('fundingMethod')} onClickReceive={onClickReceive} /> )} 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 index cf0191b3de66..e63a2902a552 100644 --- a/ui/components/app/assets/asset-list/native-token/native-token.tsx +++ b/ui/components/app/assets/asset-list/native-token/native-token.tsx @@ -8,11 +8,11 @@ import { getMultichainIsMainnet, getMultichainSelectedAccountCachedBalance, } from '../../../../../selectors/multichain'; +import { getPreferences } from '../../../../../selectors'; 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); @@ -20,6 +20,7 @@ const NativeToken = ({ onClickAsset }: AssetListProps) => { const { chainId, ticker, type, rpcUrl } = useSelector( getMultichainCurrentNetwork, ); + const { privacyMode } = useSelector(getPreferences); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, ticker, @@ -52,6 +53,7 @@ const NativeToken = ({ onClickAsset }: AssetListProps) => { isNativeCurrency isStakeable={isStakeable} showPercentage + privacyMode={privacyMode} /> ); }; diff --git a/ui/components/app/assets/asset-list/network-filter/index.scss b/ui/components/app/assets/asset-list/network-filter/index.scss new file mode 100644 index 000000000000..76e61c1025ae --- /dev/null +++ b/ui/components/app/assets/asset-list/network-filter/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/network-filter/index.ts b/ui/components/app/assets/asset-list/network-filter/index.ts new file mode 100644 index 000000000000..61bca0ca23e0 --- /dev/null +++ b/ui/components/app/assets/asset-list/network-filter/index.ts @@ -0,0 +1 @@ +export { default } from './network-filter'; diff --git a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx new file mode 100644 index 000000000000..cc2d0f38210e --- /dev/null +++ b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { setTokenNetworkFilter } from '../../../../../store/actions'; +import { + getCurrentChainId, + getCurrentNetwork, + getIsTestnet, + getPreferences, + getSelectedInternalAccount, + getShouldHideZeroBalanceTokens, + getNetworkConfigurationsByChainId, +} from '../../../../../selectors'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { SelectableListItem } from '../sort-control/sort-control'; +import { useAccountTotalFiatBalance } from '../../../../../hooks/useAccountTotalFiatBalance'; +import { Text } from '../../../../component-library/text/text'; +import { + Display, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { Box } from '../../../../component-library/box/box'; +import { AvatarNetwork } from '../../../../component-library'; +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../shared/constants/network'; + +type SortControlProps = { + handleClose: () => void; +}; + +const NetworkFilter = ({ handleClose }: SortControlProps) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const chainId = useSelector(getCurrentChainId); + const selectedAccount = useSelector(getSelectedInternalAccount); + const currentNetwork = useSelector(getCurrentNetwork); + const allNetworks = useSelector(getNetworkConfigurationsByChainId); + const isTestnet = useSelector(getIsTestnet); + const { tokenNetworkFilter, showNativeTokenAsMainBalance } = + useSelector(getPreferences); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + + const { totalFiatBalance: selectedAccountBalance } = + useAccountTotalFiatBalance(selectedAccount, shouldHideZeroBalanceTokens); + + // TODO: fetch balances across networks + // const multiNetworkAccountBalance = useMultichainAccountBalance() + + const handleFilter = (chainFilters: Record) => { + dispatch(setTokenNetworkFilter(chainFilters)); + + // TODO Add metrics + handleClose(); + }; + + return ( + <> + handleFilter({})} + > + + + + {t('allNetworks')} + + + {/* TODO: Should query cross chain account balance */} + $1,000.00 + + + + {Object.values(allNetworks) + .slice(0, 5) // only show a max of 5 icons overlapping + .map((network, index) => { + const networkImageUrl = + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + network.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ]; + return ( + + ); + })} + + + + handleFilter({ [chainId]: true })} + > + + + + {t('currentNetwork')} + + + + + + + + ); +}; + +export default NetworkFilter; 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 index c45a5488f1a6..8e216b5ed6c2 100644 --- a/ui/components/app/assets/asset-list/sort-control/sort-control.tsx +++ b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx @@ -1,13 +1,11 @@ import React, { ReactNode, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; -import { Box, Text } from '../../../../component-library'; +import { Box } 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'; @@ -45,9 +43,7 @@ export const SelectableListItem = ({ })} onClick={onClick} > - - {children} - + {children} {isSelected && ( - { - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - showRampsCard ? ( - - ) : null - ///: END:ONLY_INCLUDE_IF - } {isMainnet && !useNftDetection ? ( diff --git a/ui/components/app/assets/nfts/nfts-tab/nfts-tab.test.js b/ui/components/app/assets/nfts/nfts-tab/nfts-tab.test.js index 85f92a5344db..52acdbcd84f4 100644 --- a/ui/components/app/assets/nfts/nfts-tab/nfts-tab.test.js +++ b/ui/components/app/assets/nfts/nfts-tab/nfts-tab.test.js @@ -248,10 +248,6 @@ describe('NFT Items', () => { jest.clearAllMocks(); }); - function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - describe('NFTs Detection Notice', () => { it('should render the NFTs Detection Notice when currently selected network is Mainnet and nft detection is set to false and user has nfts', () => { render({ @@ -374,24 +370,4 @@ describe('NFT Items', () => { expect(historyPushMock).toHaveBeenCalledWith(SECURITY_ROUTE); }); }); - - describe('NFT Tab Ramps Card', () => { - it('shows the ramp card when user balance is zero', async () => { - const { queryByText } = render({ - selectedAddress: ACCOUNT_1, - balance: '0x0', - }); - // wait for spinner to be removed - await delay(3000); - expect(queryByText('Get ETH to buy NFTs')).toBeInTheDocument(); - }); - - it('does not show the ramp card when the account has a balance', () => { - const { queryByText } = render({ - selectedAddress: ACCOUNT_1, - balance: ETH_BALANCE, - }); - expect(queryByText('Get ETH to buy NFTs')).not.toBeInTheDocument(); - }); - }); }); 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 882c80964d5b..5cb4b30aea49 100644 --- a/ui/components/app/assets/token-cell/token-cell.test.tsx +++ b/ui/components/app/assets/token-cell/token-cell.test.tsx @@ -5,7 +5,7 @@ import { fireEvent } from '@testing-library/react'; import { useSelector } from 'react-redux'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; -import { getTokenList } from '../../../../selectors'; +import { getTokenList, getPreferences } from '../../../../selectors'; import { getMultichainCurrentChainId, getMultichainIsEvm, @@ -98,6 +98,9 @@ describe('Token Cell', () => { }; const useSelectorMock = useSelector; (useSelectorMock as jest.Mock).mockImplementation((selector) => { + if (selector === getPreferences) { + return { privacyMode: false }; + } if (selector === getTokenList) { return MOCK_GET_TOKEN_LIST; } diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 5f5b43d6c098..31bb388aa65b 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -12,6 +12,7 @@ type TokenCellProps = { symbol: string; string?: string; image: string; + privacyMode?: boolean; onClick?: (arg: string) => void; }; @@ -20,6 +21,7 @@ export default function TokenCell({ image, symbol, string, + privacyMode = false, onClick, }: TokenCellProps) { const tokenList = useSelector(getTokenList); @@ -51,6 +53,7 @@ export default function TokenCell({ isOriginalTokenSymbol={isOriginalTokenSymbol} address={address} showPercentage + privacyMode={privacyMode} /> ); } diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 8a107b154fb9..f0b17d686026 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -30,7 +30,8 @@ export default function TokenList({ nativeToken, }: TokenListProps) { const t = useI18nContext(); - const { tokenSortConfig } = useSelector(getPreferences); + const { tokenSortConfig, tokenNetworkFilter, privacyMode } = + useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); const nativeTokenWithBalance = useNativeTokenBalance(); @@ -52,6 +53,7 @@ export default function TokenList({ }; const sortedTokens = useMemo(() => { + // TODO filter assets by networkTokenFilter before sorting return sortAssets( [nativeTokenWithBalance, ...tokensWithBalances], tokenSortConfig, @@ -59,6 +61,7 @@ export default function TokenList({ }, [ tokensWithBalances, tokenSortConfig, + tokenNetworkFilter, conversionRate, contractExchangeRates, ]); @@ -86,6 +89,7 @@ export default function TokenList({ ); diff --git a/ui/components/app/assets/util/filter.test.ts b/ui/components/app/assets/util/filter.test.ts new file mode 100644 index 000000000000..fd5a612d590b --- /dev/null +++ b/ui/components/app/assets/util/filter.test.ts @@ -0,0 +1,98 @@ +import { filterAssets, FilterCriteria } from './filter'; + +describe('filterAssets function - balance and chainId filtering', () => { + type MockToken = { + name: string; + symbol: string; + chainId: string; // Updated to string (e.g., '0x01', '0x89') + balance: number; + }; + + const mockTokens: MockToken[] = [ + { name: 'Token1', symbol: 'T1', chainId: '0x01', balance: 100 }, + { name: 'Token2', symbol: 'T2', chainId: '0x02', balance: 50 }, + { name: 'Token3', symbol: 'T3', chainId: '0x01', balance: 200 }, + { name: 'Token4', symbol: 'T4', chainId: '0x89', balance: 150 }, + ]; + + test('filters by inclusive chainId', () => { + const criteria: FilterCriteria[] = [ + { + key: 'chainId', + opts: { '0x01': true, '0x89': true }, // ChainId must be '0x01' or '0x89' + filterCallback: 'inclusive', + }, + ]; + + const filtered = filterAssets(mockTokens, criteria); + + expect(filtered.length).toBe(3); // Should include 3 tokens with chainId '0x01' and '0x89' + expect(filtered.map((token) => token.chainId)).toEqual([ + '0x01', + '0x01', + '0x89', + ]); + }); + + test('filters tokens with balance between 100 and 150 inclusive', () => { + const criteria: FilterCriteria[] = [ + { + key: 'balance', + opts: { min: 100, max: 150 }, // Balance between 100 and 150 + filterCallback: 'range', + }, + ]; + + const filtered = filterAssets(mockTokens, criteria); + + expect(filtered.length).toBe(2); // Token1 and Token4 + expect(filtered.map((token) => token.balance)).toEqual([100, 150]); + }); + + test('filters by inclusive chainId and balance range', () => { + const criteria: FilterCriteria[] = [ + { + key: 'chainId', + opts: { '0x01': true, '0x89': true }, // ChainId must be '0x01' or '0x89' + filterCallback: 'inclusive', + }, + { + key: 'balance', + opts: { min: 100, max: 150 }, // Balance between 100 and 150 + filterCallback: 'range', + }, + ]; + + const filtered = filterAssets(mockTokens, criteria); + + expect(filtered.length).toBe(2); // Token1 and Token4 meet both criteria + }); + + test('returns no tokens if no chainId matches', () => { + const criteria: FilterCriteria[] = [ + { + key: 'chainId', + opts: { '0x04': true }, // No token with chainId '0x04' + filterCallback: 'inclusive', + }, + ]; + + const filtered = filterAssets(mockTokens, criteria); + + expect(filtered.length).toBe(0); // No matching tokens + }); + + test('returns no tokens if balance is not within range', () => { + const criteria: FilterCriteria[] = [ + { + key: 'balance', + opts: { min: 300, max: 400 }, // No token with balance between 300 and 400 + filterCallback: 'range', + }, + ]; + + const filtered = filterAssets(mockTokens, criteria); + + expect(filtered.length).toBe(0); // No matching tokens + }); +}); diff --git a/ui/components/app/assets/util/filter.ts b/ui/components/app/assets/util/filter.ts new file mode 100644 index 000000000000..20ca7cebcc58 --- /dev/null +++ b/ui/components/app/assets/util/filter.ts @@ -0,0 +1,62 @@ +import { get } from 'lodash'; + +export type FilterCriteria = { + key: string; + opts: Record; // Use opts for range, inclusion, etc. + filterCallback: FilterCallbackKeys; // Specify the type of filter: 'range', 'inclusive', etc. +}; + +export type FilterType = string | number | boolean | Date; +type FilterCallbackKeys = keyof FilterCallbacksT; + +export type FilterCallbacksT = { + inclusive: (value: string, opts: Record) => boolean; + range: (value: number, opts: Record) => boolean; +}; + +const filterCallbacks: FilterCallbacksT = { + inclusive: (value: string, opts: Record) => { + if (Object.entries(opts).length === 0) { + return false; + } + return opts[value]; + }, + range: (value: number, opts: Record) => + value >= opts.min && value <= opts.max, +}; + +function getNestedValue(obj: T, keyPath: string): FilterType { + return get(obj, keyPath); +} + +export function filterAssets(assets: T[], criteria: FilterCriteria[]): T[] { + if (criteria.length === 0) { + return assets; + } + + return assets.filter((asset) => + criteria.every(({ key, opts, filterCallback }) => { + const nestedValue = getNestedValue(asset, key); + + // If there's no callback or options, exit early and don't filter based on this criterion. + if (!filterCallback || !opts) { + return true; + } + + switch (filterCallback) { + case 'inclusive': + return filterCallbacks.inclusive( + nestedValue as string, + opts as Record, + ); + case 'range': + return filterCallbacks.range( + nestedValue as number, + opts as { min: number; max: number }, + ); + default: + return true; + } + }), + ); +} diff --git a/ui/components/app/confirm/info/row/__snapshots__/copy-icon.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/copy-icon.test.tsx.snap index 192eb016c478..947d2aae64a8 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/copy-icon.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/copy-icon.test.tsx.snap @@ -2,9 +2,15 @@ exports[`CopyIcon should match snapshot 1`] = `
- +
`; diff --git a/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap index e98ec1921081..9f7014dea03e 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap @@ -12,6 +12,7 @@ exports[`ConfirmInfoRowCurrency should display in currency passed 1`] = ` > $82.65 @@ -37,6 +38,7 @@ exports[`ConfirmInfoRowCurrency should display value in user preferred currency > 0.14861879 diff --git a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap index dd6b6f568ef8..7dad4ee50357 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap @@ -34,10 +34,16 @@ exports[`ConfirmInfoRow should match snapshot when copy is enabled 1`] = ` class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-5 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg" style="overflow-wrap: anywhere; min-height: 24px; position: relative;" > - +
diff --git a/ui/components/app/confirm/info/row/address.test.tsx b/ui/components/app/confirm/info/row/address.test.tsx index 08a3561691ff..f27e067787b1 100644 --- a/ui/components/app/confirm/info/row/address.test.tsx +++ b/ui/components/app/confirm/info/row/address.test.tsx @@ -8,6 +8,8 @@ import { mockNetworkState } from '../../../../../../test/stub/networks'; import { ConfirmInfoRowAddress } from './address'; import { TEST_ADDRESS } from './constants'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const render = ( // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -19,7 +21,10 @@ const render = ( ...storeOverrides, }); - return renderWithProvider(, store); + return renderWithProvider( + , + store, + ); }; describe('ConfirmInfoRowAddress', () => { diff --git a/ui/components/app/confirm/info/row/address.tsx b/ui/components/app/confirm/info/row/address.tsx index 7d28851ece92..95fabf26652f 100644 --- a/ui/components/app/confirm/info/row/address.tsx +++ b/ui/components/app/confirm/info/row/address.tsx @@ -22,11 +22,12 @@ import { useFallbackDisplayName } from './hook'; export type ConfirmInfoRowAddressProps = { address: string; + chainId: string; isSnapUsingThis?: boolean; }; export const ConfirmInfoRowAddress = memo( - ({ address, isSnapUsingThis }: ConfirmInfoRowAddressProps) => { + ({ address, chainId, isSnapUsingThis }: ConfirmInfoRowAddressProps) => { const isPetNamesEnabled = useSelector(getPetnamesEnabled); const { displayName, hexAddress } = useFallbackDisplayName(address); const [isNicknamePopoverShown, setIsNicknamePopoverShown] = useState(false); @@ -48,6 +49,7 @@ export const ConfirmInfoRowAddress = memo( value={hexAddress} type={NameType.ETHEREUM_ADDRESS} preferContractSymbol + variation={chainId} /> ) : ( <> diff --git a/ui/components/app/confirm/info/row/copy-icon.tsx b/ui/components/app/confirm/info/row/copy-icon.tsx index ce349089dac3..16f6604a53d4 100644 --- a/ui/components/app/confirm/info/row/copy-icon.tsx +++ b/ui/components/app/confirm/info/row/copy-icon.tsx @@ -1,12 +1,20 @@ -import React, { useCallback } from 'react'; +import React, { CSSProperties, useCallback } from 'react'; import { useCopyToClipboard } from '../../../../../hooks/useCopyToClipboard'; import { IconColor } from '../../../../../helpers/constants/design-system'; -import { Icon, IconName, IconSize } from '../../../../component-library'; +import { + ButtonIcon, + ButtonIconSize, + IconName, +} from '../../../../component-library'; type CopyCallback = (text: string) => void; -export const CopyIcon: React.FC<{ copyText: string }> = ({ copyText }) => { +export const CopyIcon: React.FC<{ + copyText: string; + color?: IconColor; + style?: CSSProperties; +}> = ({ copyText, color, style = {} }) => { const [copied, handleCopy] = useCopyToClipboard(); const handleClick = useCallback(async () => { @@ -14,12 +22,19 @@ export const CopyIcon: React.FC<{ copyText: string }> = ({ copyText }) => { }, [copyText]); return ( - ); }; diff --git a/ui/components/app/confirm/info/row/row.test.tsx b/ui/components/app/confirm/info/row/row.test.tsx index 3a6a77e4b354..8f0edab1be03 100644 --- a/ui/components/app/confirm/info/row/row.test.tsx +++ b/ui/components/app/confirm/info/row/row.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Text } from '../../../../component-library'; import { ConfirmInfoRow } from './row'; @@ -22,4 +22,20 @@ describe('ConfirmInfoRow', () => { ); expect(container).toMatchSnapshot(); }); + + it('should be expandable when collapsed is true', () => { + render( + + Some text + , + ); + expect(screen.queryByText('Some text')).not.toBeInTheDocument(); + fireEvent.click(screen.getByTestId('sectionCollapseButton')); + expect(screen.queryByText('Some text')).toBeInTheDocument(); + }); }); diff --git a/ui/components/app/confirm/info/row/row.tsx b/ui/components/app/confirm/info/row/row.tsx index 7616ccae5f21..63d2242461ea 100644 --- a/ui/components/app/confirm/info/row/row.tsx +++ b/ui/components/app/confirm/info/row/row.tsx @@ -1,7 +1,9 @@ -import React, { createContext } from 'react'; +import React, { createContext, useState } from 'react'; import Tooltip from '../../../../ui/tooltip/tooltip'; import { Box, + ButtonIcon, + ButtonIconSize, Icon, IconName, IconSize, @@ -40,6 +42,7 @@ export type ConfirmInfoRowProps = { copyEnabled?: boolean; copyText?: string; 'data-testid'?: string; + collapsed?: boolean; }; const BACKGROUND_COLORS = { @@ -79,71 +82,101 @@ export const ConfirmInfoRow: React.FC = ({ labelChildren, color, copyEnabled = false, - copyText = undefined, + copyText, 'data-testid': dataTestId, -}) => ( - - - {copyEnabled && } + collapsed, +}) => { + const [expanded, setExpanded] = useState(!collapsed); + + const isCollapsible = collapsed !== undefined; + + return ( + - - - {label} - - {labelChildren} - {!labelChildren && tooltip?.length && ( - - - - )} + {copyEnabled && ( + + )} + {isCollapsible && ( + setExpanded(!expanded)} + data-testid="sectionCollapseButton" + ariaLabel="collapse-button" + /> + )} + + + + {label} + + {labelChildren} + {!labelChildren && tooltip?.length && ( + + + + )} + + {expanded && + (typeof children === 'string' ? ( + + {children} + + ) : ( + children + ))} - {typeof children === 'string' ? ( - - {children} - - ) : ( - children - )} - - -); + + ); +}; diff --git a/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap b/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap index f9823b5af9ac..2482a916a5a9 100644 --- a/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap +++ b/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap @@ -36,6 +36,7 @@ exports[`CurrencyInput Component rendering should disable unit input 1`] = ` > $0.00 @@ -89,6 +90,7 @@ exports[`CurrencyInput Component rendering should render properly with a fiat va > 0.004327880204275946 @@ -183,6 +185,7 @@ exports[`CurrencyInput Component rendering should render properly with an ETH va > $231.06 @@ -237,6 +240,7 @@ exports[`CurrencyInput Component rendering should render properly without a suff > $0.00 diff --git a/ui/components/app/detected-token/detected-token-details/detected-token-details.js b/ui/components/app/detected-token/detected-token-details/detected-token-details.js index 3fb3219630a1..fe8f12618305 100644 --- a/ui/components/app/detected-token/detected-token-details/detected-token-details.js +++ b/ui/components/app/detected-token/detected-token-details/detected-token-details.js @@ -1,14 +1,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; - -import { Box } from '../../../component-library'; -import Identicon from '../../../ui/identicon'; +import { + AvatarNetwork, + AvatarNetworkSize, + AvatarToken, + AvatarTokenSize, + BadgeWrapper, + Box, +} from '../../../component-library'; import DetectedTokenValues from '../detected-token-values/detected-token-values'; import DetectedTokenAddress from '../detected-token-address/detected-token-address'; import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators'; import { Display } from '../../../../helpers/constants/design-system'; -import { getTokenList } from '../../../../selectors'; +import { + getCurrentNetwork, + getTestNetworkBackgroundColor, + getTokenList, +} from '../../../../selectors'; const DetectedTokenDetails = ({ token, @@ -17,6 +26,8 @@ const DetectedTokenDetails = ({ }) => { const tokenList = useSelector(getTokenList); const tokenData = tokenList[token.address?.toLowerCase()]; + const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); + const currentNetwork = useSelector(getCurrentNetwork); return ( - + } + marginRight={2} className="detected-token-details__identicon" - address={token.address} - diameter={40} - /> + > + + + <0.000001 @@ -26,6 +27,7 @@ exports[`CancelTransactionGasFee Component should render 1`] = ` > <0.000001 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 a6d0df79843d..da6f598dfa5e 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 @@ -707,13 +707,48 @@ exports[`NameDetails renders with recognized name 1`] = ` class="name name__recognized_unsaved" >
- +
+
+ + + + + +
+

({ @@ -23,6 +26,7 @@ jest.mock('../../../../store/actions', () => ({ jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useDispatch: jest.fn(), + useSelector: jest.fn(), })); jest.useFakeTimers(); @@ -37,11 +41,11 @@ const SOURCE_ID_MOCK = 'ens'; const SOURCE_ID_2_MOCK = 'some_snap'; const PROPOSED_NAME_MOCK = 'TestProposedName'; const PROPOSED_NAME_2_MOCK = 'TestProposedName2'; +const VARIATION_MOCK = CHAIN_ID_MOCK; const STATE_MOCK = { metamask: { ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - nameSources: { [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, }, @@ -85,13 +89,17 @@ const STATE_MOCK = { }, }, useTokenDetection: true, - tokenList: { - '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': { - address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d', - symbol: 'IUSD', - name: 'iZUMi Bond USD', - iconUrl: - 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', + tokensChainsCache: { + [VARIATION_MOCK]: { + data: { + '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': { + address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d', + symbol: 'IUSD', + name: 'iZUMi Bond USD', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', + }, + }, }, }, }, @@ -146,10 +154,74 @@ describe('NameDetails', () => { const setNameMock = jest.mocked(setName); const updateProposedNamesMock = jest.mocked(updateProposedNames); const useDispatchMock = jest.mocked(useDispatch); + const useSelectorMock = jest.mocked(useSelector); beforeEach(() => { jest.resetAllMocks(); useDispatchMock.mockReturnValue(jest.fn()); + + useSelectorMock.mockImplementation((selector) => { + if (selector === getNames) { + return { + [NameType.ETHEREUM_ADDRESS]: { + [ADDRESS_SAVED_NAME_MOCK]: { + [VARIATION_MOCK]: { + name: SAVED_NAME_MOCK, + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + }, + }, + [ADDRESS_NO_NAME_MOCK]: { + [VARIATION_MOCK]: { + name: SAVED_NAME_MOCK, + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + }, + }, + }, + }; + } else if (selector === getNftContractsByAddressByChain) { + return { + [VARIATION_MOCK]: { + [ADDRESS_RECOGNIZED_MOCK]: { + name: 'iZUMi Bond USD', + }, + }, + }; + } else if (selector === getDomainResolutions) { + return [ + { + resolvedAddress: ADDRESS_SAVED_NAME_MOCK, + domainName: 'Domain name', + }, + ]; + } else if (selector === getNameSources) { + return { + [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, + }; + } + return undefined; + }); }); it('renders when no address value is passed', () => { @@ -157,6 +229,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -166,10 +239,38 @@ describe('NameDetails', () => { }); it('renders with no saved name', () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getNames) { + return { + [NameType.ETHEREUM_ADDRESS]: {}, + }; + } else if (selector === getNftContractsByAddressByChain) { + return { + [VARIATION_MOCK]: { + [ADDRESS_RECOGNIZED_MOCK]: { + name: 'iZUMi Bond USD', + }, + }, + }; + } else if (selector === getDomainResolutions) { + return [ + { + resolvedAddress: ADDRESS_SAVED_NAME_MOCK, + }, + ]; + } else if (selector === getNameSources) { + return { + [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, + }; + } + return undefined; + }); + const { baseElement } = renderWithProvider( undefined} />, store, @@ -183,6 +284,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -196,6 +298,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -209,6 +312,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -229,6 +333,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -251,6 +356,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -273,6 +379,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -295,6 +402,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -317,6 +425,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -336,6 +445,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -373,6 +483,7 @@ describe('NameDetails', () => { undefined} /> , @@ -392,6 +503,50 @@ describe('NameDetails', () => { }); it('sends created event', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getNames) { + return { + [NameType.ETHEREUM_ADDRESS]: { + [ADDRESS_NO_NAME_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + }, + }, + }, + }; + } else if (selector === getNftContractsByAddressByChain) { + return { + [VARIATION_MOCK]: { + [ADDRESS_RECOGNIZED_MOCK]: { + name: 'iZUMi Bond USD', + }, + }, + }; + } else if (selector === getDomainResolutions) { + return [ + { + resolvedAddress: ADDRESS_SAVED_NAME_MOCK, + }, + ]; + } else if (selector === getNameSources) { + return { + [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, + }; + } + return undefined; + }); + const trackEventMock = jest.fn(); const component = renderWithProvider( @@ -399,6 +554,7 @@ describe('NameDetails', () => { undefined} /> , @@ -407,7 +563,7 @@ describe('NameDetails', () => { await saveNameUsingDropdown(component, PROPOSED_NAME_MOCK); - expect(trackEventMock).toHaveBeenCalledWith({ + expect(trackEventMock).toHaveBeenNthCalledWith(2, { event: MetaMetricsEventName.PetnameCreated, category: MetaMetricsEventCategory.Petnames, properties: { @@ -419,6 +575,69 @@ describe('NameDetails', () => { }); it('sends updated event', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getNames) { + return { + [NameType.ETHEREUM_ADDRESS]: { + [ADDRESS_SAVED_NAME_MOCK]: { + [CHAIN_ID_MOCK]: { + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + name: SAVED_NAME_MOCK, + sourceId: SOURCE_ID_MOCK, + }, + }, + [ADDRESS_NO_NAME_MOCK]: { + [CHAIN_ID_MOCK]: { + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + name: null, + }, + }, + }, + }; + } else if (selector === getNftContractsByAddressByChain) { + return { + [VARIATION_MOCK]: { + [ADDRESS_RECOGNIZED_MOCK]: { + name: 'iZUMi Bond USD', + }, + }, + }; + } else if (selector === getDomainResolutions) { + return [ + { + resolvedAddress: ADDRESS_SAVED_NAME_MOCK, + }, + ]; + } else if (selector === getNameSources) { + return { + [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, + }; + } + return undefined; + }); + const trackEventMock = jest.fn(); const component = renderWithProvider( @@ -426,6 +645,7 @@ describe('NameDetails', () => { undefined} /> , @@ -434,7 +654,7 @@ describe('NameDetails', () => { await saveNameUsingDropdown(component, PROPOSED_NAME_2_MOCK); - expect(trackEventMock).toHaveBeenCalledWith({ + expect(trackEventMock).toHaveBeenNthCalledWith(2, { event: MetaMetricsEventName.PetnameUpdated, category: MetaMetricsEventCategory.Petnames, properties: { @@ -447,6 +667,69 @@ describe('NameDetails', () => { }); it('sends deleted event', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getNames) { + return { + [NameType.ETHEREUM_ADDRESS]: { + [ADDRESS_SAVED_NAME_MOCK]: { + [CHAIN_ID_MOCK]: { + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + name: SAVED_NAME_MOCK, + sourceId: SOURCE_ID_MOCK, + }, + }, + [ADDRESS_NO_NAME_MOCK]: { + [CHAIN_ID_MOCK]: { + proposedNames: { + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + [SOURCE_ID_2_MOCK]: { + proposedNames: [PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + retryDelay: null, + }, + }, + name: null, + }, + }, + }, + }; + } else if (selector === getNftContractsByAddressByChain) { + return { + [VARIATION_MOCK]: { + [ADDRESS_RECOGNIZED_MOCK]: { + name: 'iZUMi Bond USD', + }, + }, + }; + } else if (selector === getDomainResolutions) { + return [ + { + resolvedAddress: ADDRESS_SAVED_NAME_MOCK, + }, + ]; + } else if (selector === getNameSources) { + return { + [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, + }; + } + return undefined; + }); + const trackEventMock = jest.fn(); const component = renderWithProvider( @@ -454,6 +737,7 @@ describe('NameDetails', () => { undefined} /> , @@ -462,7 +746,7 @@ describe('NameDetails', () => { await saveNameUsingTextField(component, ''); - expect(trackEventMock).toHaveBeenCalledWith({ + expect(trackEventMock).toHaveBeenNthCalledWith(2, { event: MetaMetricsEventName.PetnameDeleted, category: MetaMetricsEventCategory.Petnames, properties: { diff --git a/ui/components/app/name/name-details/name-details.tsx b/ui/components/app/name/name-details/name-details.tsx index 22b0445a0ad5..1bb2b1f1e478 100644 --- a/ui/components/app/name/name-details/name-details.tsx +++ b/ui/components/app/name/name-details/name-details.tsx @@ -46,7 +46,7 @@ import Name from '../name'; import FormComboField, { FormComboFieldOption, } from '../../../ui/form-combo-field/form-combo-field'; -import { getCurrentChainId, getNameSources } from '../../../../selectors'; +import { getNameSources } from '../../../../selectors'; import { setName as saveName, updateProposedNames, @@ -64,6 +64,7 @@ export type NameDetailsProps = { sourcePriority?: string[]; type: NameType; value: string; + variation: string; }; type ProposedNameOption = Required & { @@ -157,12 +158,14 @@ function getInitialSources( return [...resultSources, ...stateSources].sort(); } -function useProposedNames(value: string, type: NameType, chainId: string) { +function useProposedNames(value: string, type: NameType, variation: string) { const dispatch = useDispatch(); - const { proposedNames } = useName(value, type); + const { proposedNames } = useName(value, type, variation); + // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const updateInterval = useRef(); + const [initialSources, setInitialSources] = useState(); useEffect(() => { @@ -178,7 +181,7 @@ function useProposedNames(value: string, type: NameType, chainId: string) { value, type, onlyUpdateAfterDelay: true, - variation: chainId, + variation, }), // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -196,7 +199,7 @@ function useProposedNames(value: string, type: NameType, chainId: string) { updateInterval.current = setInterval(update, UPDATE_DELAY); return reset; - }, [value, type, chainId, dispatch, initialSources, setInitialSources]); + }, [value, type, variation, dispatch, initialSources, setInitialSources]); return { proposedNames, initialSources }; } @@ -205,13 +208,20 @@ export default function NameDetails({ onClose, type, value, + variation, }: NameDetailsProps) { - const chainId = useSelector(getCurrentChainId); - const { name: savedPetname, sourceId: savedSourceId } = useName(value, type); - const { name: displayName, hasPetname: hasSavedPetname } = useDisplayName( + const { name: savedPetname, sourceId: savedSourceId } = useName( value, type, + variation, ); + + const { name: displayName, hasPetname: hasSavedPetname } = useDisplayName({ + value, + type, + variation, + }); + const nameSources = useSelector(getNameSources, isEqual); const [name, setName] = useState(''); const [openMetricSent, setOpenMetricSent] = useState(false); @@ -226,7 +236,7 @@ export default function NameDetails({ const { proposedNames, initialSources } = useProposedNames( value, type, - chainId, + variation, ); const [copiedAddress, handleCopyAddress] = useCopyToClipboard() as [ @@ -275,12 +285,12 @@ export default function NameDetails({ type, name: name?.length ? name : null, sourceId: selectedSourceId, - variation: chainId, + variation, }), ); onClose(); - }, [name, selectedSourceId, onClose, trackPetnamesSaveEvent, chainId]); + }, [name, selectedSourceId, onClose, trackPetnamesSaveEvent, variation]); const handleClose = useCallback(() => { onClose(); @@ -333,6 +343,7 @@ export default function NameDetails({ diff --git a/ui/components/app/name/name.stories.tsx b/ui/components/app/name/name.stories.tsx index fb23334a8776..732c9059b530 100644 --- a/ui/components/app/name/name.stories.tsx +++ b/ui/components/app/name/name.stories.tsx @@ -3,108 +3,75 @@ import React from 'react'; import { NameType } from '@metamask/name-controller'; import { Provider } from 'react-redux'; import configureStore from '../../../store/store'; -import Name from './name'; -import { mockNetworkState } from '../../../../test/stub/networks'; +import Name, { NameProps } from './name'; +import mockState from '../../../../test/data/mock-state.json'; +import { + EXPERIENCES_TYPE, + FIRST_PARTY_CONTRACT_NAMES, +} from '../../../../shared/constants/first-party-contracts'; +import { cloneDeep } from 'lodash'; -const addressNoSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54978'; -const addressSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54977'; -const addressSavedTokenMock = '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d'; -const addressUnsavedTokenMock = '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8'; -const chainIdMock = '0x1'; +const ADDRESS_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54978'; +const ADDRESS_NFT_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54979'; +const VARIATION_MOCK = '0x1'; +const NAME_MOCK = 'Saved Name'; -const storeMock = configureStore({ +const ADDRESS_FIRST_PARTY_MOCK = + FIRST_PARTY_CONTRACT_NAMES[EXPERIENCES_TYPE.METAMASK_BRIDGE][ + VARIATION_MOCK + ].toLowerCase(); + +const PROPOSED_NAMES_MOCK = { + ens: { + proposedNames: ['test.eth'], + lastRequestTime: 123, + retryDelay: null, + }, + etherscan: { + proposedNames: ['TestContract'], + lastRequestTime: 123, + retryDelay: null, + }, + token: { + proposedNames: ['Test Token'], + lastRequestTime: 123, + retryDelay: null, + }, + lens: { + proposedNames: ['test.lens'], + lastRequestTime: 123, + retryDelay: null, + }, +}; + +const STATE_MOCK = { + ...mockState, metamask: { - ...mockNetworkState({chainId: chainIdMock}), + ...mockState.metamask, useTokenDetection: true, - tokenList: { - '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': { - address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d', - symbol: 'IUSD', - name: 'iZUMi Bond USD', - iconUrl: - 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', - }, - '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8': { - address: '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8', - symbol: 'USX', - name: 'dForce USD', - iconUrl: - 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8.png', - }, - }, + tokensChainsCache: {}, names: { [NameType.ETHEREUM_ADDRESS]: { - [addressNoSavedNameMock]: { - [chainIdMock]: { - proposedNames: { - ens: { - proposedNames: ['test.eth'], - lastRequestTime: 123, - retryDelay: null, - }, - etherscan: { - proposedNames: ['TestContract'], - lastRequestTime: 123, - retryDelay: null, - }, - token: { - proposedNames: ['Test Token'], - lastRequestTime: 123, - retryDelay: null, - }, - lens: { - proposedNames: ['test.lens'], - lastRequestTime: 123, - retryDelay: null, - }, - }, + [ADDRESS_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: PROPOSED_NAMES_MOCK, }, }, - [addressSavedNameMock]: { - [chainIdMock]: { - proposedNames: { - ens: { - proposedNames: ['test.eth'], - lastRequestTime: 123, - retryDelay: null, - }, - etherscan: { - proposedNames: ['TestContract'], - lastRequestTime: 123, - retryDelay: null, - }, - token: { - proposedNames: ['Test Token'], - lastRequestTime: 123, - retryDelay: null, - }, - lens: { - proposedNames: ['test.lens'], - lastRequestTime: 123, - retryDelay: null, - }, - }, - name: 'Test Token', - sourceId: 'token', + [ADDRESS_NFT_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: PROPOSED_NAMES_MOCK, }, }, - [addressSavedTokenMock]: { - [chainIdMock]: { - proposedNames: {}, - name: 'Saved Token Name', - sourceId: 'token', + [ADDRESS_FIRST_PARTY_MOCK]: { + [VARIATION_MOCK]: { + proposedNames: PROPOSED_NAMES_MOCK, }, }, }, }, - nameSources: { - ens: { label: 'Ethereum Name Service (ENS)' }, - etherscan: { label: 'Etherscan (Verified Contract Name)' }, - token: { label: 'Blockchain (Token Name)' }, - lens: { label: 'Lens Protocol' }, - }, + nameSources: {}, }, -}); +}; /** * Displays the saved name for a raw value such as an Ethereum address.

@@ -125,6 +92,10 @@ export default { description: `The type of value.

Limited to the values in the \`NameType\` enum.`, }, + variation: { + control: 'text', + description: `The variation of the value.

For example, the chain ID if the type is Ethereum address.`, + }, disableEdit: { control: 'boolean', description: `Whether to prevent the modal from opening when the component is clicked.`, @@ -134,68 +105,141 @@ export default { }, }, args: { - value: addressNoSavedNameMock, + value: ADDRESS_MOCK, type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, disableEdit: false, }, - decorators: [(story) => {story()}], + render: ({ state, ...args }) => { + const finalState = cloneDeep(STATE_MOCK); + state?.(finalState); + + return ( + + + + ); + }, }; -// eslint-disable-next-line jsdoc/require-param /** * No name has been saved for the value and type. */ -export const DefaultStory = (args) => { - return ; +export const NoSavedName = { + name: 'No Saved Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }, }; -DefaultStory.storyName = 'No Saved Name'; - /** * A name was previously saved for this value and type.

* The component will still display a modal when clicked to edit the name. */ -export const SavedNameStory = () => { - return ; +export const SavedNameStory = { + name: 'Saved Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + state: (state) => { + state.metamask.names[NameType.ETHEREUM_ADDRESS][ADDRESS_MOCK][ + VARIATION_MOCK + ].name = NAME_MOCK; + }, + }, }; -SavedNameStory.storyName = 'Saved Name'; - /** * No name was previously saved for this recognized token.

* The component will still display a modal when clicked to edit the name. */ -export const UnsavedTokenNameStory = () => { - return ( - - ); +export const DefaultTokenNameStory = { + name: 'Default ERC-20 Token Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + state: (state) => { + state.metamask.tokensChainsCache = { + [VARIATION_MOCK]: { + data: { + [ADDRESS_MOCK]: { + address: ADDRESS_MOCK, + symbol: 'IUSD', + name: 'iZUMi Bond USD', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', + }, + }, + }, + }; + }, + }, }; -UnsavedTokenNameStory.storyName = 'Unsaved Token Name'; +/** + * No name was previously saved for this watched NFT.

+ * The component will still display a modal when clicked to edit the name. + */ +export const DefaultWatchedNFTNameStory = { + name: 'Default Watched NFT Name', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + state: (state) => { + state.metamask.allNftContracts = { + '0x123': { + [VARIATION_MOCK]: [ + { + address: ADDRESS_MOCK, + name: 'Everything I Own', + }, + ], + }, + }; + }, + }, +}; /** - * A name was previously saved for this recognized token.

+ * No name was previously saved for this recognized NFT.

* The component will still display a modal when clicked to edit the name. */ -export const SavedTokenNameStory = () => { - return ( - - ); +export const DefaultNFTNameStory = { + name: 'Default NFT Name', + args: { + value: ADDRESS_NFT_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }, }; -SavedTokenNameStory.storyName = 'Saved Token Name'; +/** + * No name was previously saved for this first-party contract.

+ * The component will still display a modal when clicked to edit the name. + */ +export const DefaultFirstPartyNameStory = { + name: 'Default First-Party Name', + args: { + value: ADDRESS_FIRST_PARTY_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }, +}; /** * Clicking the component will not display a modal to edit the name. */ -export const EditDisabledStory = () => { - return ( - - ); +export const EditDisabledStory = { + name: 'Edit Disabled', + args: { + value: ADDRESS_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + disableEdit: true, + }, }; - -EditDisabledStory.storyName = 'Edit Disabled'; diff --git a/ui/components/app/name/name.test.tsx b/ui/components/app/name/name.test.tsx index 061d39e670de..33648e98e38c 100644 --- a/ui/components/app/name/name.test.tsx +++ b/ui/components/app/name/name.test.tsx @@ -22,6 +22,7 @@ jest.mock('react-redux', () => ({ const ADDRESS_NO_SAVED_NAME_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54977'; const ADDRESS_SAVED_NAME_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54979'; const SAVED_NAME_MOCK = 'TestName'; +const VARIATION_MOCK = 'testVariation'; const STATE_MOCK = { metamask: { @@ -44,7 +45,11 @@ describe('Name', () => { }); const { container } = renderWithProvider( - , + , store, ); @@ -61,6 +66,7 @@ describe('Name', () => { , store, ); @@ -75,7 +81,11 @@ describe('Name', () => { }); const { container } = renderWithProvider( - , + , store, ); @@ -90,7 +100,11 @@ describe('Name', () => { }); const { container } = renderWithProvider( - , + , store, ); @@ -114,7 +128,11 @@ describe('Name', () => { renderWithProvider( - + , store, ); diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx index 5af2851c8885..2097d21faf07 100644 --- a/ui/components/app/name/name.tsx +++ b/ui/components/app/name/name.tsx @@ -38,6 +38,12 @@ export type NameProps = { /** The raw value to display the name of. */ value: string; + + /** + * The variation of the value. + * Such as the chain ID if the `type` is an Ethereum address. + */ + variation: string; }; function formatValue(value: string, type: NameType): string { @@ -61,15 +67,17 @@ const Name = memo( disableEdit, internal, preferContractSymbol = false, + variation, }: NameProps) => { const [modalOpen, setModalOpen] = useState(false); const trackEvent = useContext(MetaMetricsContext); - const { name, hasPetname, image } = useDisplayName( + const { name, hasPetname, image } = useDisplayName({ value, type, preferContractSymbol, - ); + variation, + }); useEffect(() => { if (internal) { @@ -100,7 +108,12 @@ const Name = memo( return ( {!disableEdit && modalOpen && ( - + )}

{ const tooltipTitle = await waitFor(() => container.querySelector( - '[data-original-title="This account is not set up for use with goerli"]', + '[data-original-title="This account is not set up for use with Goerli"]', ), ); diff --git a/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap b/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap index d29236409dbc..deb95bd48555 100644 --- a/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap +++ b/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap @@ -3,7 +3,7 @@ exports[`SnapUIAddress renders Bitcoin address 1`] = `

128Lkh3...Mp8p6

@@ -56,7 +57,7 @@ exports[`SnapUIAddress renders Bitcoin address 1`] = ` exports[`SnapUIAddress renders Bitcoin address with blockie 1`] = `

128Lkh3...Mp8p6

@@ -77,7 +79,7 @@ exports[`SnapUIAddress renders Bitcoin address with blockie 1`] = ` exports[`SnapUIAddress renders Cosmos address 1`] = `

cosmos1...6hdc0

@@ -130,7 +133,7 @@ exports[`SnapUIAddress renders Cosmos address 1`] = ` exports[`SnapUIAddress renders Cosmos address with blockie 1`] = `

cosmos1...6hdc0

@@ -151,7 +155,7 @@ exports[`SnapUIAddress renders Cosmos address with blockie 1`] = ` exports[`SnapUIAddress renders Ethereum address 1`] = `

0xab16a...Bfcdb

@@ -204,7 +209,7 @@ exports[`SnapUIAddress renders Ethereum address 1`] = ` exports[`SnapUIAddress renders Ethereum address with blockie 1`] = `

0xab16a...Bfcdb

@@ -225,7 +231,7 @@ exports[`SnapUIAddress renders Ethereum address with blockie 1`] = ` exports[`SnapUIAddress renders Hedera address 1`] = `

0.0.123...zbhlt

@@ -278,7 +285,7 @@ exports[`SnapUIAddress renders Hedera address 1`] = ` exports[`SnapUIAddress renders Hedera address with blockie 1`] = `

0.0.123...zbhlt

@@ -299,7 +307,7 @@ exports[`SnapUIAddress renders Hedera address with blockie 1`] = ` exports[`SnapUIAddress renders Polkadot address 1`] = `

5hmuyxw...egmfy

@@ -352,7 +361,7 @@ exports[`SnapUIAddress renders Polkadot address 1`] = ` exports[`SnapUIAddress renders Polkadot address with blockie 1`] = `

5hmuyxw...egmfy

@@ -373,7 +383,7 @@ exports[`SnapUIAddress renders Polkadot address with blockie 1`] = ` exports[`SnapUIAddress renders Starknet address 1`] = `

0x02dd1...0ab57

@@ -426,7 +437,7 @@ exports[`SnapUIAddress renders Starknet address 1`] = ` exports[`SnapUIAddress renders Starknet address with blockie 1`] = `

0x02dd1...0ab57

@@ -447,7 +459,7 @@ exports[`SnapUIAddress renders Starknet address with blockie 1`] = ` exports[`SnapUIAddress renders legacy Ethereum address 1`] = `

0xab16a...Bfcdb

diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index 539548622135..d39cb5adf321 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -9,21 +9,29 @@ import { AlignItems, Display, TextColor, + TextVariant, } from '../../../../helpers/constants/design-system'; import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; import { SnapUIAvatar } from '../snap-ui-avatar'; +import { useDisplayName } from '../../../../hooks/snaps/useDisplayName'; export type SnapUIAddressProps = { // The address must be a CAIP-10 string. address: string; // This is not currently exposed to Snaps. avatarSize?: 'xs' | 'sm' | 'md' | 'lg'; + truncate?: boolean; + displayName?: boolean; + avatar?: boolean; }; export const SnapUIAddress: React.FunctionComponent = ({ address, avatarSize = 'md', + truncate = true, + displayName = false, + avatar = true, }) => { const caipIdentifier = useMemo(() => { if (isHexString(address)) { @@ -40,17 +48,33 @@ export const SnapUIAddress: React.FunctionComponent = ({ [caipIdentifier], ); + const name = useDisplayName(parsed); + // For EVM addresses, we make sure they are checksummed. const transformedAddress = parsed.chain.namespace === 'eip155' ? toChecksumHexAddress(parsed.address) : parsed.address; - const shortenedAddress = shortenAddress(transformedAddress); + + const formattedAddress = truncate + ? shortenAddress(transformedAddress) + : address; return ( - - - {shortenedAddress} + + {avatar && } + + {displayName && name ? name : formattedAddress} + ); }; diff --git a/ui/components/app/snaps/snap-ui-link/index.scss b/ui/components/app/snaps/snap-ui-link/index.scss new file mode 100644 index 000000000000..7d3f75f0e372 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-link/index.scss @@ -0,0 +1,11 @@ +.snap-ui-renderer__link { + & .snap-ui-renderer__address { + // Fixes an issue where the link end icon would wrap + display: inline-flex; + } + + .snap-ui-renderer__address + .mm-icon { + // This fixes an issue where the icon would be misaligned with the Address component + top: 0; + } +} diff --git a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js index a1289543fd45..58a22008a52a 100644 --- a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js +++ b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js @@ -34,7 +34,7 @@ export const SnapUILink = ({ href, children }) => { {children} @@ -51,7 +51,14 @@ export const SnapUILink = ({ href, children }) => { externalLink size={ButtonLinkSize.Inherit} display={Display.Inline} - className="snap-ui-link" + className="snap-ui-renderer__link" + style={{ + // Prevents the link from taking up the full width of the parent. + width: 'fit-content', + }} + textProps={{ + display: Display.Inline, + }} > {children} diff --git a/ui/components/app/snaps/snap-ui-renderer/components/address.ts b/ui/components/app/snaps/snap-ui-renderer/components/address.ts index 1e39966df760..908890ecef9f 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/address.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/address.ts @@ -6,5 +6,8 @@ export const address: UIComponentFactory = ({ element }) => ({ props: { address: element.props.address, avatarSize: 'xs', + truncate: element.props.truncate, + displayName: element.props.displayName, + avatar: element.props.avatar, }, }); diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss index 7e18e72c917f..d32edf726479 100644 --- a/ui/components/app/snaps/snap-ui-renderer/index.scss +++ b/ui/components/app/snaps/snap-ui-renderer/index.scss @@ -34,6 +34,10 @@ border-radius: 8px; border-color: var(--color-border-muted); + & .mm-icon { + top: 0; + } + .mm-text--overflow-wrap-anywhere { overflow-wrap: normal; } @@ -48,10 +52,6 @@ &__panel { gap: 8px; - - .mm-icon--size-inherit { - top: 0; - } } &__text { diff --git a/ui/components/app/toast-master/selectors.ts b/ui/components/app/toast-master/selectors.ts new file mode 100644 index 000000000000..b88762c3bc19 --- /dev/null +++ b/ui/components/app/toast-master/selectors.ts @@ -0,0 +1,108 @@ +import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; +import { getAlertEnabledness } from '../../../ducks/metamask/metamask'; +import { PRIVACY_POLICY_DATE } from '../../../helpers/constants/privacy-policy'; +import { + SURVEY_DATE, + SURVEY_END_TIME, + SURVEY_START_TIME, +} from '../../../helpers/constants/survey'; +import { getPermittedAccountsForCurrentTab } from '../../../selectors'; +import { MetaMaskReduxState } from '../../../store/store'; +import { getIsPrivacyToastRecent } from './utils'; + +// TODO: get this into one of the larger definitions of state type +type State = Omit & { + appState: { + showNftDetectionEnablementToast?: boolean; + }; + metamask: { + newPrivacyPolicyToastClickedOrClosed?: boolean; + newPrivacyPolicyToastShownDate?: number; + onboardingDate?: number; + showNftDetectionEnablementToast?: boolean; + surveyLinkLastClickedOrClosed?: number; + switchedNetworkNeverShowMessage?: boolean; + }; +}; + +/** + * Determines if the survey toast should be shown based on the current time, survey start and end times, and whether the survey link was last clicked or closed. + * + * @param state - The application state containing the necessary survey data. + * @returns True if the current time is between the survey start and end times and the survey link was not last clicked or closed. False otherwise. + */ +export function selectShowSurveyToast(state: State): boolean { + if (state.metamask?.surveyLinkLastClickedOrClosed) { + return false; + } + + const startTime = new Date(`${SURVEY_DATE} ${SURVEY_START_TIME}`).getTime(); + const endTime = new Date(`${SURVEY_DATE} ${SURVEY_END_TIME}`).getTime(); + const now = Date.now(); + + return now > startTime && now < endTime; +} + +/** + * Determines if the privacy policy toast should be shown based on the current date and whether the new privacy policy toast was clicked or closed. + * + * @param state - The application state containing the privacy policy data. + * @returns Boolean is True if the toast should be shown, and the number is the date the toast was last shown. + */ +export function selectShowPrivacyPolicyToast(state: State): { + showPrivacyPolicyToast: boolean; + newPrivacyPolicyToastShownDate?: number; +} { + const { + newPrivacyPolicyToastClickedOrClosed, + newPrivacyPolicyToastShownDate, + onboardingDate, + } = state.metamask || {}; + const newPrivacyPolicyDate = new Date(PRIVACY_POLICY_DATE); + const currentDate = new Date(Date.now()); + + const showPrivacyPolicyToast = + !newPrivacyPolicyToastClickedOrClosed && + currentDate >= newPrivacyPolicyDate && + getIsPrivacyToastRecent(newPrivacyPolicyToastShownDate) && + // users who onboarded before the privacy policy date should see the notice + // and + // old users who don't have onboardingDate set should see the notice + (!onboardingDate || onboardingDate < newPrivacyPolicyDate.valueOf()); + + return { showPrivacyPolicyToast, newPrivacyPolicyToastShownDate }; +} + +export function selectNftDetectionEnablementToast(state: State): boolean { + return Boolean(state.appState?.showNftDetectionEnablementToast); +} + +// If there is more than one connected account to activeTabOrigin, +// *BUT* the current account is not one of them, show the banner +export function selectShowConnectAccountToast( + state: State, + account: InternalAccount, +): boolean { + const allowShowAccountSetting = getAlertEnabledness(state).unconnectedAccount; + const connectedAccounts = getPermittedAccountsForCurrentTab(state); + const isEvmAccount = isEvmAccountType(account?.type); + + return ( + allowShowAccountSetting && + account && + state.activeTab?.origin && + isEvmAccount && + connectedAccounts.length > 0 && + !connectedAccounts.some((address) => address === account.address) + ); +} + +/** + * Retrieves user preference to never see the "Switched Network" toast + * + * @param state - Redux state object. + * @returns Boolean preference value + */ +export function selectSwitchedNetworkNeverShowMessage(state: State): boolean { + return Boolean(state.metamask.switchedNetworkNeverShowMessage); +} diff --git a/ui/components/app/toast-master/toast-master.js b/ui/components/app/toast-master/toast-master.js new file mode 100644 index 000000000000..584f1cc25983 --- /dev/null +++ b/ui/components/app/toast-master/toast-master.js @@ -0,0 +1,299 @@ +/* eslint-disable react/prop-types -- TODO: upgrade to TypeScript */ + +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router-dom'; +import { MILLISECOND, SECOND } from '../../../../shared/constants/time'; +import { + PRIVACY_POLICY_LINK, + SURVEY_LINK, +} from '../../../../shared/lib/ui-utils'; +import { + BorderColor, + BorderRadius, + IconColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { + DEFAULT_ROUTE, + REVIEW_PERMISSIONS, +} from '../../../helpers/constants/routes'; +import { getURLHost } from '../../../helpers/utils/util'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { usePrevious } from '../../../hooks/usePrevious'; +import { + getCurrentNetwork, + getOriginOfCurrentTab, + getSelectedAccount, + getSwitchedNetworkDetails, + getUseNftDetection, +} from '../../../selectors'; +import { + addPermittedAccount, + clearSwitchedNetworkDetails, + hidePermittedNetworkToast, +} from '../../../store/actions'; +import { + AvatarAccount, + AvatarAccountSize, + AvatarNetwork, + Icon, + IconName, +} from '../../component-library'; +import { Toast, ToastContainer } from '../../multichain'; +import { SurveyToast } from '../../ui/survey-toast'; +import { + selectNftDetectionEnablementToast, + selectShowConnectAccountToast, + selectShowPrivacyPolicyToast, + selectShowSurveyToast, + selectSwitchedNetworkNeverShowMessage, +} from './selectors'; +import { + setNewPrivacyPolicyToastClickedOrClosed, + setNewPrivacyPolicyToastShownDate, + setShowNftDetectionEnablementToast, + setSurveyLinkLastClickedOrClosed, + setSwitchedNetworkNeverShowMessage, +} from './utils'; + +export function ToastMaster() { + const location = useLocation(); + + const onHomeScreen = location.pathname === DEFAULT_ROUTE; + + return ( + onHomeScreen && ( + + + + + + + + + + ) + ); +} + +function ConnectAccountToast() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const [hideConnectAccountToast, setHideConnectAccountToast] = useState(false); + const account = useSelector(getSelectedAccount); + + // If the account has changed, allow the connect account toast again + const prevAccountAddress = usePrevious(account?.address); + if (account?.address !== prevAccountAddress && hideConnectAccountToast) { + setHideConnectAccountToast(false); + } + + const showConnectAccountToast = useSelector((state) => + selectShowConnectAccountToast(state, account), + ); + + const activeTabOrigin = useSelector(getOriginOfCurrentTab); + + return ( + Boolean(!hideConnectAccountToast && showConnectAccountToast) && ( + + } + text={t('accountIsntConnectedToastText', [ + account?.metadata?.name, + getURLHost(activeTabOrigin), + ])} + actionText={t('connectAccount')} + onActionClick={() => { + // Connect this account + dispatch(addPermittedAccount(activeTabOrigin, account.address)); + // Use setTimeout to prevent React re-render from + // hiding the tooltip + setTimeout(() => { + // Trigger a mouseenter on the header's connection icon + // to display the informative connection tooltip + document + .querySelector( + '[data-testid="connection-menu"] [data-tooltipped]', + ) + ?.dispatchEvent(new CustomEvent('mouseenter', {})); + }, 250 * MILLISECOND); + }} + onClose={() => setHideConnectAccountToast(true)} + /> + ) + ); +} + +function SurveyToastMayDelete() { + const t = useI18nContext(); + + const showSurveyToast = useSelector(selectShowSurveyToast); + + return ( + showSurveyToast && ( + + } + text={t('surveyTitle')} + actionText={t('surveyConversion')} + onActionClick={() => { + global.platform.openTab({ + url: SURVEY_LINK, + }); + setSurveyLinkLastClickedOrClosed(Date.now()); + }} + onClose={() => { + setSurveyLinkLastClickedOrClosed(Date.now()); + }} + /> + ) + ); +} + +function PrivacyPolicyToast() { + const t = useI18nContext(); + + const { showPrivacyPolicyToast, newPrivacyPolicyToastShownDate } = + useSelector(selectShowPrivacyPolicyToast); + + // If the privacy policy toast is shown, and there is no date set, set it + if (showPrivacyPolicyToast && !newPrivacyPolicyToastShownDate) { + setNewPrivacyPolicyToastShownDate(Date.now()); + } + + return ( + showPrivacyPolicyToast && ( + + } + text={t('newPrivacyPolicyTitle')} + actionText={t('newPrivacyPolicyActionButton')} + onActionClick={() => { + global.platform.openTab({ + url: PRIVACY_POLICY_LINK, + }); + setNewPrivacyPolicyToastClickedOrClosed(); + }} + onClose={setNewPrivacyPolicyToastClickedOrClosed} + /> + ) + ); +} + +function SwitchedNetworkToast() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const switchedNetworkDetails = useSelector(getSwitchedNetworkDetails); + const switchedNetworkNeverShowMessage = useSelector( + selectSwitchedNetworkNeverShowMessage, + ); + + const isShown = switchedNetworkDetails && !switchedNetworkNeverShowMessage; + + return ( + isShown && ( + + } + text={t('switchedNetworkToastMessage', [ + switchedNetworkDetails.nickname, + getURLHost(switchedNetworkDetails.origin), + ])} + actionText={t('switchedNetworkToastDecline')} + onActionClick={setSwitchedNetworkNeverShowMessage} + onClose={() => dispatch(clearSwitchedNetworkDetails())} + /> + ) + ); +} + +function NftEnablementToast() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const showNftEnablementToast = useSelector(selectNftDetectionEnablementToast); + const useNftDetection = useSelector(getUseNftDetection); + + const autoHideToastDelay = 5 * SECOND; + + return ( + showNftEnablementToast && + useNftDetection && ( + + } + text={t('nftAutoDetectionEnabled')} + borderRadius={BorderRadius.LG} + textVariant={TextVariant.bodyMd} + autoHideTime={autoHideToastDelay} + onAutoHideToast={() => + dispatch(setShowNftDetectionEnablementToast(false)) + } + /> + ) + ); +} + +function PermittedNetworkToast() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const isPermittedNetworkToastOpen = useSelector( + (state) => state.appState.showPermittedNetworkToastOpen, + ); + + const currentNetwork = useSelector(getCurrentNetwork); + const activeTabOrigin = useSelector(getOriginOfCurrentTab); + const safeEncodedHost = encodeURIComponent(activeTabOrigin); + const history = useHistory(); + + return ( + isPermittedNetworkToastOpen && ( + + } + text={t('permittedChainToastUpdate', [ + getURLHost(activeTabOrigin), + currentNetwork?.nickname, + ])} + actionText={t('editPermissions')} + onActionClick={() => { + dispatch(hidePermittedNetworkToast()); + history.push(`${REVIEW_PERMISSIONS}/${safeEncodedHost}`); + }} + onClose={() => dispatch(hidePermittedNetworkToast())} + /> + ) + ); +} diff --git a/ui/components/app/toast-master/toast-master.test.ts b/ui/components/app/toast-master/toast-master.test.ts new file mode 100644 index 000000000000..8b29f20a240d --- /dev/null +++ b/ui/components/app/toast-master/toast-master.test.ts @@ -0,0 +1,206 @@ +import { PRIVACY_POLICY_DATE } from '../../../helpers/constants/privacy-policy'; +import { SURVEY_DATE, SURVEY_GMT } from '../../../helpers/constants/survey'; +import { + selectShowPrivacyPolicyToast, + selectShowSurveyToast, +} from './selectors'; + +describe('#getShowSurveyToast', () => { + const realDateNow = Date.now; + + afterEach(() => { + Date.now = realDateNow; + }); + + it('shows the survey link when not yet seen and within time bounds', () => { + Date.now = () => + new Date(`${SURVEY_DATE} 12:25:00 ${SURVEY_GMT}`).getTime(); + const result = selectShowSurveyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + surveyLinkLastClickedOrClosed: undefined, + }, + }); + expect(result).toStrictEqual(true); + }); + + it('does not show the survey link when seen and within time bounds', () => { + Date.now = () => + new Date(`${SURVEY_DATE} 12:25:00 ${SURVEY_GMT}`).getTime(); + const result = selectShowSurveyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + surveyLinkLastClickedOrClosed: 123456789, + }, + }); + expect(result).toStrictEqual(false); + }); + + it('does not show the survey link before time bounds', () => { + Date.now = () => + new Date(`${SURVEY_DATE} 11:25:00 ${SURVEY_GMT}`).getTime(); + const result = selectShowSurveyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + surveyLinkLastClickedOrClosed: undefined, + }, + }); + expect(result).toStrictEqual(false); + }); + + it('does not show the survey link after time bounds', () => { + Date.now = () => + new Date(`${SURVEY_DATE} 14:25:00 ${SURVEY_GMT}`).getTime(); + const result = selectShowSurveyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + surveyLinkLastClickedOrClosed: undefined, + }, + }); + expect(result).toStrictEqual(false); + }); +}); + +describe('#getShowPrivacyPolicyToast', () => { + let dateNowSpy: jest.SpyInstance; + + describe('mock one day after', () => { + beforeEach(() => { + const dayAfterPolicyDate = new Date(PRIVACY_POLICY_DATE); + dayAfterPolicyDate.setDate(dayAfterPolicyDate.getDate() + 1); + + dateNowSpy = jest + .spyOn(Date, 'now') + .mockReturnValue(dayAfterPolicyDate.getTime()); + }); + + afterEach(() => { + dateNowSpy.mockRestore(); + }); + + it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is before the policy date', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: false, + onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate( + new Date(PRIVACY_POLICY_DATE).getDate() - 2, + ), + }, + }); + expect(result.showPrivacyPolicyToast).toBe(true); + }); + + it('does not show the privacy policy toast when seen, even if on or after the policy date and onboardingDate is before the policy date', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: true, + onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate( + new Date(PRIVACY_POLICY_DATE).getDate() - 2, + ), + }, + }); + expect(result.showPrivacyPolicyToast).toBe(false); + }); + + it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is not set', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: false, + onboardingDate: undefined, + }, + }); + expect(result.showPrivacyPolicyToast).toBe(true); + }); + }); + + describe('mock same day', () => { + beforeEach(() => { + dateNowSpy = jest + .spyOn(Date, 'now') + .mockReturnValue(new Date(PRIVACY_POLICY_DATE).getTime()); + }); + + afterEach(() => { + dateNowSpy.mockRestore(); + }); + + it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is before the policy date', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: false, + onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate( + new Date(PRIVACY_POLICY_DATE).getDate() - 2, + ), + }, + }); + expect(result.showPrivacyPolicyToast).toBe(true); + }); + + it('does not show the privacy policy toast when seen, even if on or after the policy date and onboardingDate is before the policy date', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: true, + onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate( + new Date(PRIVACY_POLICY_DATE).getDate() - 2, + ), + }, + }); + expect(result.showPrivacyPolicyToast).toBe(false); + }); + + it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is not set', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: false, + onboardingDate: undefined, + }, + }); + expect(result.showPrivacyPolicyToast).toBe(true); + }); + }); + + describe('mock day before', () => { + beforeEach(() => { + const dayBeforePolicyDate = new Date(PRIVACY_POLICY_DATE); + dayBeforePolicyDate.setDate(dayBeforePolicyDate.getDate() - 1); + + dateNowSpy = jest + .spyOn(Date, 'now') + .mockReturnValue(dayBeforePolicyDate.getTime()); + }); + + afterEach(() => { + dateNowSpy.mockRestore(); + }); + + it('does not show the privacy policy toast before the policy date', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: false, + onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate( + new Date(PRIVACY_POLICY_DATE).getDate() - 2, + ), + }, + }); + expect(result.showPrivacyPolicyToast).toBe(false); + }); + + it('does not show the privacy policy toast before the policy date even if onboardingDate is not set', () => { + const result = selectShowPrivacyPolicyToast({ + // @ts-expect-error: intentionally passing incomplete input + metamask: { + newPrivacyPolicyToastClickedOrClosed: false, + onboardingDate: undefined, + }, + }); + expect(result.showPrivacyPolicyToast).toBe(false); + }); + }); +}); diff --git a/ui/components/app/toast-master/utils.ts b/ui/components/app/toast-master/utils.ts new file mode 100644 index 000000000000..d6544707f45d --- /dev/null +++ b/ui/components/app/toast-master/utils.ts @@ -0,0 +1,69 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { ReactFragment } from 'react'; +import { SHOW_NFT_DETECTION_ENABLEMENT_TOAST } from '../../../store/actionConstants'; +import { submitRequestToBackground } from '../../../store/background-connection'; + +/** + * Returns true if the privacy policy toast was shown either never, or less than a day ago. + * + * @param newPrivacyPolicyToastShownDate + * @returns true if the privacy policy toast was shown either never, or less than a day ago + */ +export function getIsPrivacyToastRecent( + newPrivacyPolicyToastShownDate?: number, +): boolean { + if (!newPrivacyPolicyToastShownDate) { + return true; + } + + const currentDate = new Date(); + const oneDayInMilliseconds = 24 * 60 * 60 * 1000; + const newPrivacyPolicyToastShownDateObj = new Date( + newPrivacyPolicyToastShownDate, + ); + const toastWasShownLessThanADayAgo = + currentDate.valueOf() - newPrivacyPolicyToastShownDateObj.valueOf() < + oneDayInMilliseconds; + + return toastWasShownLessThanADayAgo; +} + +export function setNewPrivacyPolicyToastShownDate(time: number) { + submitRequestToBackgroundAndCatch('setNewPrivacyPolicyToastShownDate', [ + time, + ]); +} + +export function setNewPrivacyPolicyToastClickedOrClosed() { + submitRequestToBackgroundAndCatch('setNewPrivacyPolicyToastClickedOrClosed'); +} + +export function setShowNftDetectionEnablementToast( + value: boolean, +): PayloadAction { + return { + type: SHOW_NFT_DETECTION_ENABLEMENT_TOAST, + payload: value, + }; +} + +export function setSwitchedNetworkNeverShowMessage() { + submitRequestToBackgroundAndCatch('setSwitchedNetworkNeverShowMessage', [ + true, + ]); +} + +export function setSurveyLinkLastClickedOrClosed(time: number) { + submitRequestToBackgroundAndCatch('setSurveyLinkLastClickedOrClosed', [time]); +} + +// May move this to a different file after discussion with team +export function submitRequestToBackgroundAndCatch( + method: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any[], +) { + submitRequestToBackground(method, args)?.catch((error) => { + console.error('Error caught in submitRequestToBackground', error); + }); +} diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 751b0f53e73a..226a2a9113c0 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -214,7 +214,7 @@ export default class TransactionListItemDetails extends PureComponent { primaryTransaction: transaction, initialTransaction: { type }, } = transactionGroup; - const { hash } = transaction; + const { chainId, hash } = transaction; return ( @@ -332,6 +332,7 @@ export default class TransactionListItemDetails extends PureComponent { recipientMetadataName={recipientMetadataName} senderName={senderNickname} senderAddress={senderAddress} + chainId={chainId} onRecipientClick={() => { this.context.trackEvent({ category: MetaMetricsEventCategory.Navigation, diff --git a/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap b/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap index b29efce542e3..4a9fc4d3cf7a 100644 --- a/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap +++ b/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap @@ -8,6 +8,7 @@ exports[`UserPreferencedCurrencyDisplay Component rendering should match snapsho > 0 diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 4db61d568f4a..779309858a18 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -16,6 +16,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showCurrencySuffix?: boolean; shouldCheckShowNativeToken?: boolean; isAggregatedFiatOverviewBalance?: boolean; + privacyMode?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 613b731d0a16..a466f7813672 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -28,6 +28,7 @@ export default function UserPreferencedCurrencyDisplay({ showNative, showCurrencySuffix, shouldCheckShowNativeToken, + privacyMode = false, ...restProps }) { // NOTE: When displaying currencies, we need the actual account to detect whether we're in a @@ -83,6 +84,7 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals={numberOfDecimals} prefixComponent={prefixComponent} suffix={showCurrencySuffix && !showEthLogo && currency} + privacyMode={privacyMode} /> ); } @@ -126,6 +128,7 @@ const UserPreferencedCurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, shouldCheckShowNativeToken: PropTypes.bool, + privacyMode: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx index 95e0d92fa2b8..8da096151908 100644 --- a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx @@ -7,6 +7,7 @@ import { getSelectedAccount, getShouldHideZeroBalanceTokens, getTokensMarketData, + getPreferences, } from '../../../selectors'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; @@ -22,6 +23,7 @@ jest.mock('../../../ducks/locale/locale', () => ({ jest.mock('../../../selectors', () => ({ getCurrentCurrency: jest.fn(), getSelectedAccount: jest.fn(), + getPreferences: jest.fn(), getShouldHideZeroBalanceTokens: jest.fn(), getTokensMarketData: jest.fn(), })); @@ -32,6 +34,7 @@ jest.mock('../../../hooks/useAccountTotalFiatBalance', () => ({ const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock; const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock; +const mockGetPreferences = getPreferences as jest.Mock; const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock; const mockGetShouldHideZeroBalanceTokens = getShouldHideZeroBalanceTokens as jest.Mock; @@ -159,6 +162,7 @@ describe('AggregatedPercentageOverview', () => { beforeEach(() => { mockGetIntlLocale.mockReturnValue('en-US'); mockGetCurrentCurrency.mockReturnValue('USD'); + mockGetPreferences.mockReturnValue({ privacyMode: false }); mockGetSelectedAccount.mockReturnValue(selectedAccountMock); mockGetShouldHideZeroBalanceTokens.mockReturnValue(false); mockGetTokensMarketData.mockReturnValue(marketDataMock); diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx index 94555d3bc0cd..8c609610daa1 100644 --- a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx @@ -7,6 +7,7 @@ import { getSelectedAccount, getShouldHideZeroBalanceTokens, getTokensMarketData, + getPreferences, } from '../../../selectors'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; @@ -19,7 +20,7 @@ import { TextColor, TextVariant, } from '../../../helpers/constants/design-system'; -import { Box, Text } from '../../component-library'; +import { Box, SensitiveText } from '../../component-library'; import { getCalculatedTokenAmount1dAgo } from '../../../helpers/utils/util'; // core already has this exported type but its not yet available in this version @@ -34,6 +35,7 @@ export const AggregatedPercentageOverview = () => { useSelector(getTokensMarketData); const locale = useSelector(getIntlLocale); const fiatCurrency = useSelector(getCurrentCurrency); + const { privacyMode } = useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, @@ -110,7 +112,7 @@ export const AggregatedPercentageOverview = () => { let color = TextColor.textDefault; - if (isValidAmount(amountChange)) { + if (!privacyMode && isValidAmount(amountChange)) { if ((amountChange as number) === 0) { color = TextColor.textDefault; } else if ((amountChange as number) > 0) { @@ -118,26 +120,33 @@ export const AggregatedPercentageOverview = () => { } else { color = TextColor.errorDefault; } + } else { + color = TextColor.textAlternative; } + return ( - {formattedAmountChange} - - + {formattedPercentChange} - + ); }; diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index abff2cb2b239..ffaed244e958 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -233,4 +233,34 @@ describe('BtcOverview', () => { const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE); expect(receiveButton).toBeInTheDocument(); }); + + it('"Buy & Sell" button is disabled for testnet accounts', () => { + const storeWithBtcBuyable = getStore({ + metamask: { + ...mockMetamaskStore, + internalAccounts: { + ...mockMetamaskStore.internalAccounts, + accounts: { + [mockNonEvmAccount.id]: { + ...mockNonEvmAccount, + address: 'tb1q9lakrt5sw0w0twnc6ww4vxs7hm0q23e03286k8', + }, + }, + }, + }, + ramps: { + buyableChains: mockBuyableChainsWithBtc, + }, + }); + + const { queryByTestId } = renderWithProvider( + , + storeWithBtcBuyable, + ); + + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + + expect(buyButton).toBeInTheDocument(); + expect(buyButton).toBeDisabled(); + }); }); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx index dc47df7567b5..2ddaefd92f58 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + getMultichainIsMainnet, + ///: END:ONLY_INCLUDE_IF getMultichainProviderConfig, getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsBitcoinBuyable } from '../../../ducks/ramps'; +import { getSelectedInternalAccount } from '../../../selectors'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; ///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; @@ -17,6 +22,11 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { const { chainId } = useSelector(getMultichainProviderConfig); const balance = useSelector(getMultichainSelectedAccountCachedBalance); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const selectedAccount = useSelector(getSelectedInternalAccount); + const isBtcMainnetAccount = useMultichainSelector( + getMultichainIsMainnet, + selectedAccount, + ); const isBtcBuyable = useSelector(getIsBitcoinBuyable); ///: END:ONLY_INCLUDE_IF @@ -31,7 +41,7 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} - isBuyableChain={isBtcBuyable} + isBuyableChain={isBtcBuyable && isBtcMainnetAccount} ///: END:ONLY_INCLUDE_IF /> ); diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 2de787ef23c0..93d9e1061428 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -28,6 +28,7 @@ import { JustifyContent, TextAlign, TextVariant, + IconColor, } from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; @@ -61,7 +62,10 @@ import Spinner from '../../ui/spinner'; import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; import { getMultichainIsEvm } from '../../../selectors/multichain'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; -import { setAggregatedBalancePopoverShown } from '../../../store/actions'; +import { + setAggregatedBalancePopoverShown, + setPrivacyMode, +} from '../../../store/actions'; import { useTheme } from '../../../hooks/useTheme'; import { getSpecificSettingsRoute } from '../../../helpers/utils/settings-search'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -128,7 +132,8 @@ export const CoinOverview = ({ const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover); const isTestnet = useSelector(getIsTestnet); - const { showFiatInTestnets } = useSelector(getPreferences); + const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } = + useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( @@ -139,8 +144,6 @@ export const CoinOverview = ({ shouldHideZeroBalanceTokens, ); - const { showNativeTokenAsMainBalance } = useSelector(getPreferences); - const isEvm = useSelector(getMultichainIsEvm); const isNotAggregatedFiatBalance = showNativeTokenAsMainBalance || isTestnet || !isEvm; @@ -163,6 +166,10 @@ export const CoinOverview = ({ dispatch(setAggregatedBalancePopoverShown()); }; + const handleSensitiveToggle = () => { + dispatch(setPrivacyMode(!privacyMode)); + }; + const [referenceElement, setReferenceElement] = useState(null); const setBoxRef = (ref: HTMLSpanElement | null) => { @@ -253,26 +260,39 @@ export const CoinOverview = ({ ref={setBoxRef} > {balanceToDisplay ? ( - + <> + + + ) : ( )} diff --git a/ui/components/app/wallet-overview/index.scss b/ui/components/app/wallet-overview/index.scss index 318c26501097..47dc40200e69 100644 --- a/ui/components/app/wallet-overview/index.scss +++ b/ui/components/app/wallet-overview/index.scss @@ -78,7 +78,8 @@ display: flex; max-width: inherit; justify-content: center; - flex-wrap: wrap; + align-items: center; + flex-wrap: nowrap; } &__primary-balance { @@ -142,7 +143,8 @@ display: flex; max-width: inherit; justify-content: center; - flex-wrap: wrap; + align-items: center; + flex-wrap: nowrap; } &__primary-balance { diff --git a/ui/components/component-library/README.md b/ui/components/component-library/README.md index ec5006d3491f..a1f865bfe95a 100644 --- a/ui/components/component-library/README.md +++ b/ui/components/component-library/README.md @@ -4,7 +4,7 @@ This folder contains design system components that are built 1:1 with the Figma ## Architecture -All components are built on top of the `Box` component and accept all `Box` [component props](/docs/components-componentlibrary-box--docs#props). +All components are built on top of the `Box` component and accept all `Box` component props. ### Layout diff --git a/ui/components/component-library/icon/icon.types.ts b/ui/components/component-library/icon/icon.types.ts index 9c87851d0b65..3afd17ef983b 100644 --- a/ui/components/component-library/icon/icon.types.ts +++ b/ui/components/component-library/icon/icon.types.ts @@ -44,6 +44,7 @@ export enum IconName { Book = 'book', Bookmark = 'bookmark', Bridge = 'bridge', + Collapse = 'collapse', Calculator = 'calculator', CardPos = 'card-pos', CardToken = 'card-token', diff --git a/ui/components/component-library/index.ts b/ui/components/component-library/index.ts index 861fb80bcf2c..634af093a41b 100644 --- a/ui/components/component-library/index.ts +++ b/ui/components/component-library/index.ts @@ -69,6 +69,8 @@ export { TagUrl } from './tag-url'; export type { TagUrlProps } from './tag-url'; export { Text, ValidTag, TextDirection, InvisibleCharacter } from './text'; export type { TextProps } from './text'; +export { SensitiveText, SensitiveTextLength } from './sensitive-text'; +export type { SensitiveTextProps } from './sensitive-text'; export { Input, InputType } from './input'; export type { InputProps } from './input'; export { TextField, TextFieldType, TextFieldSize } from './text-field'; diff --git a/ui/components/component-library/sensitive-text/README.mdx b/ui/components/component-library/sensitive-text/README.mdx new file mode 100644 index 000000000000..9e950381e6f3 --- /dev/null +++ b/ui/components/component-library/sensitive-text/README.mdx @@ -0,0 +1,81 @@ +import { Controls, Canvas } from '@storybook/blocks'; + +import * as SensitiveTextStories from './sensitive-text.stories'; + +# SensitiveText + +SensitiveText is a component that extends the Text component to handle sensitive information. It provides the ability to hide or show the text content, replacing it with dots when hidden. + + + +## Props + +The `SensitiveText` component extends the `Text` component. See the `Text` component for an extended list of props. + + + +### Children + +The text content to be displayed or hidden. + + + +```jsx +import { SensitiveText } from '../../component-library'; + + + Sensitive Information + +``` + + +### IsHidden + +Use the `isHidden` prop to determine whether the text should be hidden or visible. When `isHidden` is `true`, the component will display dots instead of the actual text. + + + +```jsx +import { SensitiveText } from '../../component-library'; + + + Sensitive Information + +``` + +### Length + +Use the `length` prop to determine the length of the hidden text (number of dots). Can be a predefined `SensitiveTextLength` or a custom string number. + +The following predefined length options are available: + +- `SensitiveTextLength.Short`: `6` +- `SensitiveTextLength.Medium`: `9` +- `SensitiveTextLength.Long`: `12` +- `SensitiveTextLength.ExtraLong`: `20` + +- The number of dots displayed is determined by the `length` prop. +- If an invalid `length` is provided, the component will fall back to `SensitiveTextLength.Short` and log a warning. +- Custom length values can be provided as strings, e.g. `15`. + + + +```jsx +import { SensitiveText, SensitiveTextLength } from '../../component-library'; + + + Length "short" (6 characters) + + + Length "medium" (9 characters) + + + Length "long" (12 characters) + + + Length "extra long" (20 characters) + + + Length "15" (15 characters) + +``` diff --git a/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap b/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap new file mode 100644 index 000000000000..6844feb1783e --- /dev/null +++ b/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SensitiveText should render correctly 1`] = ` +
+

+ Sensitive Information +

+
+`; diff --git a/ui/components/component-library/sensitive-text/index.ts b/ui/components/component-library/sensitive-text/index.ts new file mode 100644 index 000000000000..ff89896fd03b --- /dev/null +++ b/ui/components/component-library/sensitive-text/index.ts @@ -0,0 +1,3 @@ +export { SensitiveText } from './sensitive-text'; +export { SensitiveTextLength } from './sensitive-text.types'; +export type { SensitiveTextProps } from './sensitive-text.types'; diff --git a/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx b/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx new file mode 100644 index 000000000000..142def9118b5 --- /dev/null +++ b/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { SensitiveText } from '.'; +import { SensitiveTextLength } from './sensitive-text.types'; +import README from './README.mdx'; +import { Box } from '../box'; +import { + Display, + FlexDirection, +} from '../../../helpers/constants/design-system'; + +const meta: Meta = { + title: 'Components/ComponentLibrary/SensitiveText', + component: SensitiveText, + parameters: { + docs: { + page: README, + }, + }, + args: { + children: 'Sensitive information', + isHidden: false, + length: SensitiveTextLength.Short, + }, +} as Meta; + +export default meta; +type Story = StoryObj; + +export const DefaultStory: Story = {}; +DefaultStory.storyName = 'Default'; + +export const Children: Story = { + args: { + children: 'Sensitive information', + }, + render: (args) => ( + + ), +}; + +export const IsHidden: Story = { + args: { + isHidden: true, + }, + render: (args) => ( + + ), +}; + +export const Length: Story = { + args: { + isHidden: true, + }, + render: (args) => ( + + + Length "short" (6 characters) + + + Length "medium" (9 characters) + + + Length "long" (12 characters) + + + Length "extra long" (20 characters) + + + Length "15" (15 characters) + + + ), +}; diff --git a/ui/components/component-library/sensitive-text/sensitive-text.test.tsx b/ui/components/component-library/sensitive-text/sensitive-text.test.tsx new file mode 100644 index 000000000000..a4be911ea78d --- /dev/null +++ b/ui/components/component-library/sensitive-text/sensitive-text.test.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { SensitiveText } from './sensitive-text'; +import { SensitiveTextLength } from './sensitive-text.types'; + +describe('SensitiveText', () => { + const testProps = { + isHidden: false, + length: SensitiveTextLength.Short, + children: 'Sensitive Information', + }; + + it('should render correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should display the text when isHidden is false', () => { + render(); + expect(screen.getByText('Sensitive Information')).toBeInTheDocument(); + }); + + it('should hide the text when isHidden is true', () => { + render(); + expect(screen.queryByText('Sensitive Information')).not.toBeInTheDocument(); + expect(screen.getByText('••••••')).toBeInTheDocument(); + }); + + it('should render the correct number of bullets for different lengths', () => { + const lengths = [ + SensitiveTextLength.Short, + SensitiveTextLength.Medium, + SensitiveTextLength.Long, + SensitiveTextLength.ExtraLong, + ]; + + lengths.forEach((length) => { + render(); + expect(screen.getByText('•'.repeat(Number(length)))).toBeInTheDocument(); + }); + }); + + it('should handle all predefined SensitiveTextLength values', () => { + Object.entries(SensitiveTextLength).forEach(([_, value]) => { + render(); + expect(screen.getByText('•'.repeat(Number(value)))).toBeInTheDocument(); + }); + }); + + it('should handle custom length as a string', () => { + render(); + expect(screen.getByText('•'.repeat(15))).toBeInTheDocument(); + }); + + it('should fall back to Short length for invalid custom length', () => { + render(); + expect( + screen.getByText('•'.repeat(Number(SensitiveTextLength.Short))), + ).toBeInTheDocument(); + }); + + it('should log a warning for invalid custom length', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + render(); + expect(consoleSpy).toHaveBeenCalledWith( + 'Invalid length provided: abc. Falling back to Short.', + ); + consoleSpy.mockRestore(); + }); + + it('should apply additional props to the Text component', () => { + render(); + expect(screen.getByTestId('sensitive-text')).toBeInTheDocument(); + }); + + it('should forward ref to the Text component', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLParagraphElement); + }); +}); diff --git a/ui/components/component-library/sensitive-text/sensitive-text.tsx b/ui/components/component-library/sensitive-text/sensitive-text.tsx new file mode 100644 index 000000000000..ddabda784fe1 --- /dev/null +++ b/ui/components/component-library/sensitive-text/sensitive-text.tsx @@ -0,0 +1,48 @@ +import React, { useMemo } from 'react'; +import { Text } from '../text'; +import { + SensitiveTextProps, + SensitiveTextLength, +} from './sensitive-text.types'; + +export const SensitiveText = React.forwardRef< + HTMLParagraphElement, + SensitiveTextProps +>((props, ref) => { + const { + isHidden = false, + length = SensitiveTextLength.Short, + children = '', + ...restProps + } = props; + + const getFallbackLength = useMemo( + () => (len: string) => { + const numLength = Number(len); + return Number.isNaN(numLength) ? 0 : numLength; + }, + [], + ); + + const isValidCustomLength = (value: string): boolean => { + const num = Number(value); + return !Number.isNaN(num) && num > 0; + }; + + let adjustedLength = length; + if (!(length in SensitiveTextLength) && !isValidCustomLength(length)) { + console.warn(`Invalid length provided: ${length}. Falling back to Short.`); + adjustedLength = SensitiveTextLength.Short; + } + + const fallback = useMemo( + () => '•'.repeat(getFallbackLength(adjustedLength)), + [length, getFallbackLength], + ); + + return ( + + {isHidden ? fallback : children} + + ); +}); diff --git a/ui/components/component-library/sensitive-text/sensitive-text.types.ts b/ui/components/component-library/sensitive-text/sensitive-text.types.ts new file mode 100644 index 000000000000..1ea8270d377f --- /dev/null +++ b/ui/components/component-library/sensitive-text/sensitive-text.types.ts @@ -0,0 +1,44 @@ +import type { TextProps } from '../text/text.types'; + +/** + * SensitiveText length options. + */ +export const SensitiveTextLength = { + Short: '6', + Medium: '9', + Long: '12', + ExtraLong: '20', +} as const; + +/** + * Type for SensitiveTextLength values. + */ +export type SensitiveTextLengthType = + (typeof SensitiveTextLength)[keyof typeof SensitiveTextLength]; +/** + * Type for custom length values. + */ +export type CustomLength = string; + +export type SensitiveTextProps = Omit< + TextProps, + 'children' +> & { + /** + * Boolean to determine whether the text should be hidden or visible. + * + * @default false + */ + isHidden?: boolean; + /** + * Determines the length of the hidden text (number of asterisks). + * Can be a predefined SensitiveTextLength or a custom string number. + * + * @default SensitiveTextLength.Short + */ + length?: SensitiveTextLengthType | CustomLength; + /** + * The text content to be displayed or hidden. + */ + children?: React.ReactNode; +}; diff --git a/ui/components/component-library/text/README.mdx b/ui/components/component-library/text/README.mdx index 5b275aa48e23..183fcbbca9f8 100644 --- a/ui/components/component-library/text/README.mdx +++ b/ui/components/component-library/text/README.mdx @@ -580,7 +580,7 @@ Values using the `TextAlign` object from `./ui/helpers/constants/design-system.j ### Box Props -Box props are now integrated with the `Text` component. Valid Box props: [Box](/docs/components-componentlibrary-box--docs#props) +Box props are now integrated with the `Text` component. You no longer need to pass these props as an object through `boxProps` diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx index 06fe1336ca7e..e76dabc7add7 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx @@ -4,7 +4,7 @@ import { ICustodianType } from '@metamask-institutional/types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { hideModal } from '../../../store/actions'; -import { getSelectedInternalAccount } from '../../../selectors/selectors'; +import { getSelectedInternalAccount } from '../../../selectors/accounts'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { Box, diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx index 10dc049b8678..7c1d0f60488f 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx @@ -56,6 +56,7 @@ const InteractiveReplacementTokenNotification: React.FC< interactiveReplacementToken && Boolean(Object.keys(interactiveReplacementToken).length); + // @ts-expect-error keyring type is wrong maybe? if (!/^Custody/u.test(keyring.type) || !hasInteractiveReplacementToken) { setShowNotification(false); return; @@ -66,6 +67,7 @@ const InteractiveReplacementTokenNotification: React.FC< )) as unknown as string; const custodyAccountDetails = await dispatch( mmiActions.getAllCustodianAccountsWithToken( + // @ts-expect-error keyring type is wrong maybe? keyring.type.split(' - ')[1], token, ), @@ -105,6 +107,7 @@ const InteractiveReplacementTokenNotification: React.FC< interactiveReplacementToken?.oldRefreshToken, isUnlocked, dispatch, + // @ts-expect-error keyring type is wrong maybe? keyring.type, interactiveReplacementToken, mmiActions, diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap index 51f6f2e905f9..e320bd1de0e3 100644 --- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -242,6 +242,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > $100,000.00 @@ -538,6 +539,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > 0.006 @@ -581,6 +583,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > 0.006 diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index 517639b1c86e..143c4d142a16 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -86,6 +86,8 @@ const AccountListItem = ({ isActive = false, startAccessory, onActionClick, + shouldScrollToWhenSelected = true, + privacyMode = false, }) => { const t = useI18nContext(); const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); @@ -128,10 +130,10 @@ const AccountListItem = ({ // scroll the item into view const itemRef = useRef(null); useEffect(() => { - if (selected) { + if (selected && shouldScrollToWhenSelected) { itemRef.current?.scrollIntoView?.(); } - }, [itemRef, selected]); + }, [itemRef, selected, shouldScrollToWhenSelected]); const trackEvent = useContext(MetaMetricsContext); const primaryTokenImage = useMultichainSelector( @@ -312,6 +314,7 @@ const AccountListItem = ({ type={PRIMARY} showFiat={showFiat} data-testid="first-currency-display" + privacyMode={privacyMode} />
@@ -359,6 +362,7 @@ const AccountListItem = ({ type={SECONDARY} showNative data-testid="second-currency-display" + privacyMode={privacyMode} /> @@ -502,6 +506,14 @@ AccountListItem.propTypes = { * Represents start accessory */ startAccessory: PropTypes.node, + /** + * Determines if list item should be scrolled to when selected + */ + shouldScrollToWhenSelected: PropTypes.bool, + /** + * Determines if list balance should be obfuscated + */ + privacyMode: PropTypes.bool, }; AccountListItem.displayName = 'AccountListItem'; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index 19d313aedf54..cfb49d246ca6 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -188,6 +188,7 @@ export const mergeAccounts = ( type AccountListMenuProps = { onClose: () => void; + privacyMode?: boolean; showAccountCreation?: boolean; accountListItemProps?: object; allowedAccountTypes?: KeyringAccountType[]; @@ -195,6 +196,7 @@ type AccountListMenuProps = { export const AccountListMenu = ({ onClose, + privacyMode = false, showAccountCreation = true, accountListItemProps, allowedAccountTypes = [ @@ -455,6 +457,7 @@ export const AccountListMenu = ({ { trackEvent({ category: MetaMetricsEventCategory.Navigation, @@ -643,6 +646,7 @@ export const AccountListMenu = ({ isHidden={Boolean(account.hidden)} currentTabOrigin={currentTabOrigin} isActive={Boolean(account.active)} + privacyMode={privacyMode} {...accountListItemProps} /> diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap index d22597edd89f..247f7aeb5c78 100644 --- a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap +++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap @@ -616,6 +616,7 @@ exports[`App Header unlocked state matches snapshot: unlocked 1`] = ` >

1

diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap index 9c0bd9c49482..a0c808186082 100644 --- a/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap @@ -8,6 +8,7 @@ exports[`AssetBalanceText matches snapshot 1`] = ` > prefix-fiat value diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap index d53c8e7d8d8a..b4a4836db2d6 100644 --- a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap +++ b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap @@ -358,6 +358,7 @@ exports[`Connect More Accounts Modal should render correctly 1`] = ` > 0 @@ -401,6 +402,7 @@ exports[`Connect More Accounts Modal should render correctly 1`] = ` > 0 diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx index 509a4aa60a2a..34ec98e671b9 100644 --- a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx +++ b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx @@ -57,7 +57,7 @@ describe('FundingMethodModal', () => { expect(queryByTestId('funding-method-modal')).toBeNull(); }); - it('should call openBuyCryptoInPdapp when the Buy Crypto item is clicked', () => { + it('should call openBuyCryptoInPdapp when the Token Marketplace item is clicked', () => { const { getByText } = renderWithProvider( { store, ); - fireEvent.click(getByText('Buy crypto')); + fireEvent.click(getByText('Token marketplace')); expect(openBuyCryptoInPdapp).toHaveBeenCalled(); }); diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx index 47d6ed22c2e8..baa0e234a32a 100644 --- a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx +++ b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx @@ -115,8 +115,8 @@ export const FundingMethodModal: React.FC = ({ { handleNotificationsClick()} + data-testid="notifications-menu-item" > {rpcEndpoint.name ?? new URL(rpcEndpoint.url).host} diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 5376dc17859e..518f9f19387a 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -251,10 +251,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { const generateNetworkListItem = (network: NetworkConfiguration) => { const isCurrentNetwork = network.chainId === currentChainId; const canDeleteNetwork = - isUnlocked && - !isCurrentNetwork && - network.chainId !== CHAIN_IDS.MAINNET && - network.chainId !== CHAIN_IDS.LINEA_MAINNET; + isUnlocked && !isCurrentNetwork && network.chainId !== CHAIN_IDS.MAINNET; return ( {notificationsUnreadCount > 10 ? '9+' : notificationsUnreadCount} diff --git a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap index afd02098086f..ad2dc490d7c0 100644 --- a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap +++ b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap @@ -297,6 +297,7 @@ exports[`Connections Content should render correctly 1`] = ` > 966.988 @@ -340,6 +341,7 @@ exports[`Connections Content should render correctly 1`] = ` > 966.988 diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index ae7a93283ead..d5ca0b816d48 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -82,6 +82,15 @@ export const SiteCell: React.FC = ({ ]) : t('requestingFor'); + const networkMessageConnectedState = + selectedChainIdsLength === 1 + ? t('connectedWithNetworkName', [selectedNetworks[0].name]) + : t('connectedWithNetwork', [selectedChainIdsLength]); + const networkMessageNotConnectedState = + selectedChainIdsLength === 1 + ? t('requestingForNetwork', [selectedNetworks[0].name]) + : t('requestingFor'); + return ( <> = ({ setShowEditAccountsModal(true); trackEvent({ category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.TokenImportButtonClicked, + event: MetaMetricsEventName.ViewPermissionedAccounts, properties: { location: 'Connect view, Permissions toast, Permissions (dapp)', }, @@ -124,16 +133,14 @@ export const SiteCell: React.FC = ({ { setShowEditNetworksModal(true); trackEvent({ category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.TokenImportButtonClicked, + event: MetaMetricsEventName.ViewPermissionedNetworks, properties: { location: 'Connect view, Permissions toast, Permissions (dapp)', }, diff --git a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap index 814dc934fc9a..7b0605b7ea60 100644 --- a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap +++ b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap @@ -474,6 +474,7 @@ exports[`SendPage render and initialization should render correctly even when a > $0.00 @@ -517,6 +518,7 @@ exports[`SendPage render and initialization should render correctly even when a > 0 diff --git a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap index 9fbf7e29879b..71431a330f94 100644 --- a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap +++ b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap @@ -248,6 +248,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 966.988 @@ -291,6 +292,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 966.988 @@ -545,6 +547,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -588,6 +591,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -842,6 +846,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -885,6 +890,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1148,6 +1154,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1191,6 +1198,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1445,6 +1453,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1488,6 +1497,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1755,6 +1765,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1798,6 +1809,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 diff --git a/ui/components/multichain/pages/send/components/your-accounts.tsx b/ui/components/multichain/pages/send/components/your-accounts.tsx index f53d6603cb78..e59d0aa2d5a1 100644 --- a/ui/components/multichain/pages/send/components/your-accounts.tsx +++ b/ui/components/multichain/pages/send/components/your-accounts.tsx @@ -57,6 +57,7 @@ export const SendPageYourAccounts = ({ selected={selectedAccount.address === account.address} key={account.address} isPinned={Boolean(account.pinned)} + shouldScrollToWhenSelected={false} onClick={() => { dispatch( addHistoryEntry( diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index ac72f4c5112f..5fb91272ff83 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -30,7 +30,6 @@ const darkenGradient = export const RAMPS_CARD_VARIANT_TYPES = { TOKEN: 'token', - NFT: 'nft', ACTIVITY: 'activity', BTC: 'btc', }; @@ -41,15 +40,8 @@ export const RAMPS_CARD_VARIANTS = { gradient: // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #0189EC 0%, #4B7AED 35%, #6774EE 58%, #706AF4 80.5%, #7C5BFC 100%)', - title: 'fundYourWallet', - body: 'getStartedByFundingWallet', - }, - [RAMPS_CARD_VARIANT_TYPES.NFT]: { - illustrationSrc: './images/ramps-card-nft-illustration.png', - // eslint-disable-next-line @metamask/design-tokens/color-no-hex - gradient: 'linear-gradient(90deg, #F6822D 0%, #F894A7 52%, #ED94FB 92.5%)', - title: 'getStartedWithNFTs', - body: 'getStartedWithNFTsDescription', + title: 'tipsForUsingAWallet', + body: 'tipsForUsingAWalletDescription', }, [RAMPS_CARD_VARIANT_TYPES.ACTIVITY]: { illustrationSrc: './images/ramps-card-activity-illustration.png', @@ -57,22 +49,21 @@ export const RAMPS_CARD_VARIANTS = { // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #57C5DC 0%, #06BFDD 49.39%, #35A9C7 100%)', - title: 'startYourJourney', - body: 'startYourJourneyDescription', + title: 'tipsForUsingAWallet', + body: 'tipsForUsingAWalletDescription', }, [RAMPS_CARD_VARIANT_TYPES.BTC]: { illustrationSrc: './images/ramps-card-btc-illustration.png', gradient: // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #017ED9 0%, #446FD9 35%, #5E6AD9 58%, #635ED9 80.5%, #6855D9 92.5%, #6A4FD9 100%)', - title: 'fundYourWallet', - body: 'fundYourWalletDescription', + title: 'tipsForUsingAWallet', + body: 'tipsForUsingAWalletDescription', }, }; const metamaskEntryMap = { [RAMPS_CARD_VARIANT_TYPES.TOKEN]: RampsMetaMaskEntry.TokensBanner, - [RAMPS_CARD_VARIANT_TYPES.NFT]: RampsMetaMaskEntry.NftBanner, [RAMPS_CARD_VARIANT_TYPES.ACTIVITY]: RampsMetaMaskEntry.ActivityBanner, [RAMPS_CARD_VARIANT_TYPES.BTC]: RampsMetaMaskEntry.BtcBanner, }; @@ -87,8 +78,6 @@ export const RampsCard = ({ variant, handleOnClick }) => { const { chainId, nickname } = useSelector(getMultichainCurrentNetwork); const { symbol } = useSelector(getMultichainDefaultToken); - const isTokenVariant = variant === RAMPS_CARD_VARIANT_TYPES.TOKEN; - useEffect(() => { trackEvent({ event: MetaMetricsEventName.EmptyBuyBannerDisplayed, @@ -110,7 +99,7 @@ export const RampsCard = ({ variant, handleOnClick }) => { category: MetaMetricsEventCategory.Navigation, properties: { location: `${variant} tab`, - text: `Buy ${symbol}`, + text: `Token Marketplace`, // FIXME: This might not be a number for non-EVM networks chain_id: chainId, token_symbol: symbol, @@ -132,14 +121,14 @@ export const RampsCard = ({ variant, handleOnClick }) => { }} > - {t(title, [symbol])} + {t(title)} - {t(body, [symbol])} + {t(body)} - {isTokenVariant ? t('getStarted') : t('buyToken', [symbol])} + {t('tokenMarketplace')} ); diff --git a/ui/components/multichain/ramps-card/ramps-card.stories.js b/ui/components/multichain/ramps-card/ramps-card.stories.js index 2a4dce444c7e..903ea3d27f9a 100644 --- a/ui/components/multichain/ramps-card/ramps-card.stories.js +++ b/ui/components/multichain/ramps-card/ramps-card.stories.js @@ -24,12 +24,6 @@ export const TokensStory = (args) => ( TokensStory.storyName = 'Tokens'; -export const NFTsStory = (args) => ( - -); - -NFTsStory.storyName = 'NFTs'; - export const ActivityStory = (args) => ( ); diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index 0c3c46114541..bf3968963465 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -34,6 +34,8 @@ import { ModalFooter, ModalHeader, ModalOverlay, + SensitiveText, + SensitiveTextLength, Text, } from '../../component-library'; import { @@ -82,6 +84,7 @@ type TokenListItemProps = { address?: string | null; showPercentage?: boolean; isPrimaryTokenSymbolHidden?: boolean; + privacyMode?: boolean; }; export const TokenListItem = ({ @@ -99,6 +102,7 @@ export const TokenListItem = ({ isStakeable = false, address = null, showPercentage = false, + privacyMode = false, }: TokenListItemProps) => { const t = useI18nContext(); const isEvm = useSelector(getMultichainIsEvm); @@ -375,17 +379,19 @@ export const TokenListItem = ({ ariaLabel={''} /> - {primary}{' '} {isNativeCurrency || isPrimaryTokenSymbolHidden ? '' : tokenSymbol} - + ) : ( - {secondary} - - + {primary}{' '} {isNativeCurrency || isPrimaryTokenSymbolHidden ? '' : tokenSymbol} - + )} diff --git a/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap b/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap index 44ba7be60b6f..eeb40144894b 100644 --- a/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap +++ b/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap @@ -8,6 +8,7 @@ exports[`CurrencyDisplay Component should match default snapshot 1`] = ` >
@@ -21,6 +22,7 @@ exports[`CurrencyDisplay Component should render text with a className 1`] = ` > $123.45 @@ -36,6 +38,7 @@ exports[`CurrencyDisplay Component should render text with a prefix 1`] = ` > - $123.45 diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index ca9322661d79..7e2569ffaee3 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { EtherDenomination } from '../../../../shared/constants/common'; -import { Text, Box } from '../../component-library'; +import { SensitiveText, Box } from '../../component-library'; import { AlignItems, Display, @@ -33,6 +33,7 @@ export default function CurrencyDisplay({ textProps = {}, suffixProps = {}, isAggregatedFiatOverviewBalance = false, + privacyMode = false, ...props }) { const [title, parts] = useCurrencyDisplay(value, { @@ -68,26 +69,33 @@ export default function CurrencyDisplay({ {prefixComponent} ) : null} - {parts.prefix} {parts.value} - + {parts.suffix ? ( - {parts.suffix} - + ) : null} ); @@ -115,6 +123,7 @@ const CurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, isAggregatedFiatOverviewBalance: PropTypes.bool, + privacyMode: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/components/ui/definition-list/definition-list.js b/ui/components/ui/definition-list/definition-list.js index 84a23325b37a..84d3f48135ab 100644 --- a/ui/components/ui/definition-list/definition-list.js +++ b/ui/components/ui/definition-list/definition-list.js @@ -32,7 +32,7 @@ export default function DefinitionList({ {Object.entries(dictionary).map(([term, definition]) => ( ) : (
) : (
@@ -292,4 +297,5 @@ SenderToRecipient.propTypes = { onSenderClick: PropTypes.func, warnUserOnAccountMismatch: PropTypes.bool, recipientIsOwnedAccount: PropTypes.bool, + chainId: PropTypes.string, }; diff --git a/ui/components/ui/token-currency-display/token-currency-display.stories.tsx b/ui/components/ui/token-currency-display/token-currency-display.stories.tsx index 7cf850c42c84..932d54210b84 100644 --- a/ui/components/ui/token-currency-display/token-currency-display.stories.tsx +++ b/ui/components/ui/token-currency-display/token-currency-display.stories.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { Meta, Story } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react'; import TokenCurrencyDisplay from './token-currency-display.component'; -import { TokenCurrencyDisplayProps } from './token-currency-display.types'; -export default { +const meta: Meta = { title: 'Components/UI/TokenCurrencyDisplay', component: TokenCurrencyDisplay, argTypes: { @@ -12,14 +11,15 @@ export default { token: { control: 'object' }, prefix: { control: 'text' }, }, -} as Meta; + args: { + className: '', + transactionData: '0x123', + token: { symbol: 'ETH' }, + prefix: '', + }, +}; -const Template: Story = (args) => ; +export default meta; +type Story = StoryObj; -export const Default = Template.bind({}); -Default.args = { - className: '', - transactionData: '0x123', - token: { symbol: 'ETH' }, - prefix: '', -}; +export const Default: Story = {}; diff --git a/ui/components/ui/truncated-definition-list/truncated-definition-list.js b/ui/components/ui/truncated-definition-list/truncated-definition-list.js index ae1782979866..2db9784dad8e 100644 --- a/ui/components/ui/truncated-definition-list/truncated-definition-list.js +++ b/ui/components/ui/truncated-definition-list/truncated-definition-list.js @@ -5,7 +5,6 @@ import { BorderColor, Size } from '../../../helpers/constants/design-system'; import Box from '../box'; import Button from '../button'; import DefinitionList from '../definition-list/definition-list'; -import Popover from '../popover'; import { useI18nContext } from '../../../hooks/useI18nContext'; export default function TruncatedDefinitionList({ @@ -13,7 +12,6 @@ export default function TruncatedDefinitionList({ tooltips, warnings, prefaceKeys, - title, }) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const t = useI18nContext(); @@ -33,55 +31,27 @@ export default function TruncatedDefinitionList({ type="link" onClick={() => setIsPopoverOpen(true)} > - {t(process.env.CHAIN_PERMISSIONS ? 'seeDetails' : 'viewAllDetails')} + {t('seeDetails')} ); - const renderPopover = () => - isPopoverOpen && ( - setIsPopoverOpen(false)} - footer={ - - } - > - - {renderDefinitionList(true)} - - - ); - const renderContent = () => { - if (process.env.CHAIN_PERMISSIONS) { - return isPopoverOpen ? ( - renderDefinitionList(true) - ) : ( - <> - {renderDefinitionList(false)} - {renderButton()} - - ); - } - return ( + return isPopoverOpen ? ( + renderDefinitionList(true) + ) : ( <> {renderDefinitionList(false)} {renderButton()} - {renderPopover()} ); }; return ( { + useCurrencyRatePolling(); + useTokenRatesPolling(); + + return <>{children}; +}; diff --git a/ui/contexts/currencyRate.js b/ui/contexts/currencyRate.js deleted file mode 100644 index 6739b730a882..000000000000 --- a/ui/contexts/currencyRate.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; - -export const CurrencyRateProvider = ({ children }) => { - useCurrencyRatePolling(); - - return <>{children}; -}; - -CurrencyRateProvider.propTypes = { - children: PropTypes.node, -}; diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 9a7a93ea958b..27b20a5841b3 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -339,4 +339,23 @@ describe('App State', () => { expect(state.showDataDeletionErrorModal).toStrictEqual(false); }); + + it('displays error in settings', () => { + const state = reduceApp(metamaskState, { + type: actions.SHOW_SETTINGS_PAGE_ERROR, + payload: 'settings page error', + }); + + expect(state.errorInSettings).toStrictEqual('settings page error'); + }); + + it('hides error in settings', () => { + const displayErrorInSettings = { errorInSettings: 'settings page error' }; + const oldState = { ...metamaskState, ...displayErrorInSettings }; + const state = reduceApp(oldState, { + type: actions.HIDE_SETTINGS_PAGE_ERROR, + }); + + expect(state.errorInSettings).toBeNull(); + }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index e6a7855ce7a5..81f875446f1e 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -105,6 +105,7 @@ type AppState = { snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; + errorInSettings: string | null; }; export type AppSliceState = { @@ -192,6 +193,7 @@ const initialState: AppState = { snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, + errorInSettings: null, }; export default function reduceApp( @@ -632,6 +634,16 @@ export default function reduceApp( ...appState, showDataDeletionErrorModal: false, }; + case actionConstants.SHOW_SETTINGS_PAGE_ERROR: + return { + ...appState, + errorInSettings: action.payload, + }; + case actionConstants.HIDE_SETTINGS_PAGE_ERROR: + return { + ...appState, + errorInSettings: null, + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -720,6 +732,27 @@ export function setCustomTokenAmount(payload: string): PayloadAction { return { type: actionConstants.SET_CUSTOM_TOKEN_AMOUNT, payload }; } +/** + * An action creator for display a error to the user in various places in the + * UI. It will not be cleared until a new warning replaces it or `hideWarning` + * is called. + * + * @param payload - The warning to show. + * @returns The action to display the warning. + */ +export function displayErrorInSettings(payload: string): PayloadAction { + return { + type: actionConstants.SHOW_SETTINGS_PAGE_ERROR, + payload, + }; +} + +export function hideErrorInSettings() { + return { + type: actionConstants.HIDE_SETTINGS_PAGE_ERROR, + }; +} + // Selectors export function getQrCodeData(state: AppSliceState): { type?: string | null; diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 5e50b004b774..c3854bdc4ae8 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -11,31 +11,31 @@ import { import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; +import { QuoteRequest } from '../../pages/bridge/types'; import { bridgeSlice } from './bridge'; const { - setToChainId: setToChainId_, + setToChainId, setFromToken, setToToken, setFromTokenInputValue, resetInputFields, - switchToAndFromTokens, } = bridgeSlice.actions; export { - setFromToken, + setToChainId, + resetInputFields, setToToken, + setFromToken, setFromTokenInputValue, - switchToAndFromTokens, - resetInputFields, }; const callBridgeControllerMethod = ( bridgeAction: BridgeUserAction | BridgeBackgroundAction, - args?: T[], + args?: T, ) => { return async (dispatch: MetaMaskReduxDispatch) => { - await submitRequestToBackground(bridgeAction, args); + await submitRequestToBackground(bridgeAction, [args]); await forceUpdateMetamaskState(dispatch); }; }; @@ -53,20 +53,29 @@ export const setBridgeFeatureFlags = () => { export const setFromChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { dispatch( - callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [ + callBridgeControllerMethod( + BridgeUserAction.SELECT_SRC_NETWORK, chainId, - ]), + ), ); }; }; export const setToChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { - dispatch(setToChainId_(chainId)); dispatch( - callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [ + callBridgeControllerMethod( + BridgeUserAction.SELECT_DEST_NETWORK, chainId, - ]), + ), + ); + }; +}; + +export const updateQuoteRequestParams = (params: Partial) => { + return async (dispatch: MetaMaskReduxDispatch) => { + await dispatch( + callBridgeControllerMethod(BridgeUserAction.UPDATE_QUOTE_PARAMS, params), ); }; }; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index f4a566c233b5..6b85565c6143 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -1,5 +1,6 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { zeroAddress } from 'ethereumjs-util'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { setBackgroundConnection } from '../../store/background-connection'; @@ -18,7 +19,8 @@ import { setToToken, setFromChain, resetInputFields, - switchToAndFromTokens, + setToChainId, + updateQuoteRequestParams, } from './actions'; const middleware = [thunk]; @@ -31,11 +33,25 @@ describe('Ducks - Bridge', () => { store.clearActions(); }); - describe('setToChain', () => { - it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => { + describe('setToChainId', () => { + it('calls the "bridge/setToChainId" action', () => { const state = store.getState().bridge; const actionPayload = CHAIN_IDS.OPTIMISM; + store.dispatch(setToChainId(actionPayload as never) as never); + + // Check redux state + const actions = store.getActions(); + expect(actions[0].type).toStrictEqual('bridge/setToChainId'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toChainId).toStrictEqual(actionPayload); + }); + }); + + describe('setToChain', () => { + it('calls the selectDestNetwork background action', () => { + const actionPayload = CHAIN_IDS.OPTIMISM; + const mockSelectDestNetwork = jest.fn().mockReturnValue({}); setBackgroundConnection({ [BridgeUserAction.SELECT_DEST_NETWORK]: mockSelectDestNetwork, @@ -43,11 +59,6 @@ describe('Ducks - Bridge', () => { store.dispatch(setToChain(actionPayload as never) as never); - // Check redux state - const actions = store.getActions(); - expect(actions[0].type).toStrictEqual('bridge/setToChainId'); - const newState = bridgeReducer(state, actions[0]); - expect(newState.toChainId).toStrictEqual(actionPayload); // Check background state expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); expect(mockSelectDestNetwork).toHaveBeenCalledWith( @@ -61,7 +72,7 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; - store.dispatch(setFromToken(actionPayload)); + store.dispatch(setFromToken(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromToken'); const newState = bridgeReducer(state, actions[0]); @@ -73,7 +84,8 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setToToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; - store.dispatch(setToToken(actionPayload)); + + store.dispatch(setToToken(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setToToken'); const newState = bridgeReducer(state, actions[0]); @@ -85,7 +97,8 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromTokenInputValue" action', () => { const state = store.getState().bridge; const actionPayload = '10'; - store.dispatch(setFromTokenInputValue(actionPayload)); + + store.dispatch(setFromTokenInputValue(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue'); const newState = bridgeReducer(state, actions[0]); @@ -137,31 +150,30 @@ describe('Ducks - Bridge', () => { }); }); - describe('switchToAndFromTokens', () => { - it('switches to and from input values', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bridgeStore = configureMockStore(middleware)( - createBridgeMockStore( - {}, - { - toChainId: CHAIN_IDS.MAINNET, - fromToken: { symbol: 'WETH', address: '0x13341432' }, - toToken: { symbol: 'USDC', address: '0x13341431' }, - fromTokenInputValue: '10', - }, - ), + describe('updateQuoteRequestParams', () => { + it('dispatches quote params to the bridge controller', () => { + const mockUpdateParams = jest.fn(); + setBackgroundConnection({ + [BridgeUserAction.UPDATE_QUOTE_PARAMS]: mockUpdateParams, + } as never); + + store.dispatch( + updateQuoteRequestParams({ + srcChainId: 1, + srcTokenAddress: zeroAddress(), + destTokenAddress: undefined, + }) as never, + ); + + expect(mockUpdateParams).toHaveBeenCalledTimes(1); + expect(mockUpdateParams).toHaveBeenCalledWith( + { + srcChainId: 1, + srcTokenAddress: zeroAddress(), + destTokenAddress: undefined, + }, + expect.anything(), ); - const state = bridgeStore.getState().bridge; - bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON)); - const actions = bridgeStore.getActions(); - expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens'); - const newState = bridgeReducer(state, actions[0]); - expect(newState).toStrictEqual({ - toChainId: CHAIN_IDS.POLYGON, - fromToken: { symbol: 'USDC', address: '0x13341431' }, - toToken: { symbol: 'WETH', address: '0x13341432' }, - fromTokenInputValue: null, - }); }); }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 9ec744d9e953..c75030c7591d 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -39,12 +39,6 @@ const bridgeSlice = createSlice({ resetInputFields: () => ({ ...initialState, }), - switchToAndFromTokens: (state, { payload }) => ({ - toChainId: payload, - fromToken: state.toToken, - toToken: state.fromToken, - fromTokenInputValue: null, - }), }, }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index cf27790aa943..dbb49a20d66b 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -30,7 +30,7 @@ describe('Bridge selectors', () => { { srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] }, { toChainId: '0xe708' }, {}, - { ...mockNetworkState(FEATURED_RPCS[0]) }, + { ...mockNetworkState(FEATURED_RPCS[1]) }, ); const result = getFromChain(state as never); @@ -89,7 +89,7 @@ describe('Bridge selectors', () => { ); const result = getAllBridgeableNetworks(state as never); - expect(result).toHaveLength(7); + expect(result).toHaveLength(8); expect(result[0]).toStrictEqual( expect.objectContaining({ chainId: FEATURED_RPCS[0].chainId }), ); @@ -190,21 +190,19 @@ describe('Bridge selectors', () => { }, {}, {}, - mockNetworkState(...FEATURED_RPCS, { - chainId: CHAIN_IDS.LINEA_MAINNET, - }), + mockNetworkState(...FEATURED_RPCS), ); const result = getToChains(state as never); expect(result).toHaveLength(3); expect(result[0]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), + expect.objectContaining({ chainId: CHAIN_IDS.ARBITRUM }), ); expect(result[1]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), + expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }), ); expect(result[2]).toStrictEqual( - expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }), + expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }), ); }); @@ -297,7 +295,9 @@ describe('Bridge selectors', () => { { ...mockNetworkState( ...Object.values(BUILT_IN_NETWORKS), - ...FEATURED_RPCS, + ...FEATURED_RPCS.filter( + (network) => network.chainId !== CHAIN_IDS.LINEA_MAINNET, // Linea mainnet is both a built in network, as well as featured RPC + ), ), useExternalServices: true, }, @@ -395,7 +395,7 @@ describe('Bridge selectors', () => { const state = createBridgeMockStore(); const result = getToAmount(state as never); - expect(result).toStrictEqual('0'); + expect(result).toStrictEqual(undefined); }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 8cd56928fc66..a05640ded5c2 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -3,12 +3,13 @@ import { NetworkState, } from '@metamask/network-controller'; import { uniqBy } from 'lodash'; +import { createSelector } from 'reselect'; import { getNetworkConfigurationsByChainId, getIsBridgeEnabled, getSwapsDefaultToken, SwapsEthToken, -} from '../../selectors'; +} from '../../selectors/selectors'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { BridgeControllerState, @@ -19,6 +20,10 @@ import { import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { BridgeState } from './bridge'; type BridgeAppState = { @@ -110,7 +115,7 @@ export const getToTokens = (state: BridgeAppState) => { export const getFromToken = ( state: BridgeAppState, -): SwapsTokenObject | SwapsEthToken => { +): SwapsTokenObject | SwapsEthToken | null => { return state.bridge.fromToken?.address ? state.bridge.fromToken : getSwapsDefaultToken(state); @@ -124,10 +129,38 @@ export const getToToken = ( export const getFromAmount = (state: BridgeAppState): string | null => state.bridge.fromTokenInputValue; -export const getToAmount = (_state: BridgeAppState) => { - return '0'; + +export const getBridgeQuotes = (state: BridgeAppState) => { + return { + quotes: state.metamask.bridgeState.quotes, + quotesLastFetchedMs: state.metamask.bridgeState.quotesLastFetched, + isLoading: + state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING, + }; +}; + +export const getRecommendedQuote = createSelector( + getBridgeQuotes, + ({ quotes }) => { + // TODO implement sorting + return quotes[0]; + }, +); + +export const getQuoteRequest = (state: BridgeAppState) => { + const { quoteRequest } = state.metamask.bridgeState; + return quoteRequest; }; +export const getToAmount = createSelector(getRecommendedQuote, (quote) => + quote + ? calcTokenAmount( + quote.quote.destTokenAmount, + quote.quote.destAsset.decimals, + ) + : undefined, +); + export const getIsBridgeTx = createDeepEqualSelector( getFromChain, getToChain, diff --git a/ui/ducks/locale/locale.js b/ui/ducks/locale/locale.js deleted file mode 100644 index 5118a749ab58..000000000000 --- a/ui/ducks/locale/locale.js +++ /dev/null @@ -1,42 +0,0 @@ -import { createSelector } from 'reselect'; -import * as actionConstants from '../../store/actionConstants'; - -export default function reduceLocaleMessages(state = {}, { type, payload }) { - switch (type) { - case actionConstants.SET_CURRENT_LOCALE: - return { - ...state, - current: payload.messages, - currentLocale: payload.locale, - }; - default: - return state; - } -} - -/** - * This selector returns a code from file://./../../../app/_locales/index.json. - * - * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. - * - * @param state - * @returns {string} the user's selected locale. - * These codes are not safe to use with the Intl API. - */ -export const getCurrentLocale = (state) => state.localeMessages.currentLocale; - -/** - * This selector returns a - * [BCP 47 Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) - * for use with the Intl API. - * - * @returns {Intl.UnicodeBCP47LocaleIdentifier} the user's selected locale. - */ -export const getIntlLocale = createSelector( - getCurrentLocale, - (locale) => Intl.getCanonicalLocales(locale?.replace(/_/gu, '-'))[0], -); - -export const getCurrentLocaleMessages = (state) => state.localeMessages.current; - -export const getEnLocaleMessages = (state) => state.localeMessages.en; diff --git a/ui/ducks/locale/locale.test.ts b/ui/ducks/locale/locale.test.ts index 37fc1f99e29a..67627bb73423 100644 --- a/ui/ducks/locale/locale.test.ts +++ b/ui/ducks/locale/locale.test.ts @@ -1,32 +1,80 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import locales from '../../../app/_locales/index.json'; -import { getIntlLocale } from './locale'; +import testData from '../../../test/data/mock-state.json'; +import { + getCurrentLocale, + getIntlLocale, + getCurrentLocaleMessages, + getEnLocaleMessages, +} from './locale'; + +// Mock state creation functions const createMockStateWithLocale = (locale: string) => ({ localeMessages: { currentLocale: locale }, }); -describe('getIntlLocale', () => { - it('returns the canonical BCP 47 language tag for the currently selected locale', () => { - const mockState = createMockStateWithLocale('ab-cd'); +describe('Locale Selectors', () => { + describe('getCurrentLocale', () => { + it('returns the current locale from the state', () => { + expect(getCurrentLocale(testData)).toBe('en'); + }); - expect(getIntlLocale(mockState)).toBe('ab-CD'); + it('returns undefined if no current locale is set', () => { + const newAppState = { + ...testData, + localeMessages: { + currentLocale: undefined, + }, + }; + expect(getCurrentLocale(newAppState)).toBeUndefined(); + }); }); - it('throws an error if locale cannot be made into BCP 47 language tag', () => { - const mockState = createMockStateWithLocale('xxxinvalid-locale'); + describe('getIntlLocale', () => { + it('returns the canonical BCP 47 language tag for the currently selected locale', () => { + const mockState = createMockStateWithLocale('en_US'); + expect(getIntlLocale(mockState)).toBe('en-US'); + }); - expect(() => getIntlLocale(mockState)).toThrow(); + locales.forEach((locale: { code: string; name: string }) => { + it(`handles supported locale - "${locale.code}"`, () => { + const mockState = createMockStateWithLocale(locale.code); + expect(() => getIntlLocale(mockState)).not.toThrow(); + }); + }); }); - // @ts-expect-error This is missing from the Mocha type definitions - it.each(locales)( - 'handles all supported locales – "%s"', - (locale: { code: string; name: string }) => { - const mockState = createMockStateWithLocale(locale.code); + describe('getCurrentLocaleMessages', () => { + it('returns the current locale messages from the state', () => { + expect(getCurrentLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no current locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + current: undefined, + }, + }; + expect(getCurrentLocaleMessages(newAppState)).toEqual(undefined); + }); + }); - expect(() => getIntlLocale(mockState)).not.toThrow(); - }, - ); + describe('getEnLocaleMessages', () => { + it('returns the English locale messages from the state', () => { + expect(getEnLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no English locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + en: undefined, + }, + }; + expect(getEnLocaleMessages(newAppState)).toBeUndefined(); + }); + }); }); diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts new file mode 100644 index 000000000000..2ca0cbaac4dc --- /dev/null +++ b/ui/ducks/locale/locale.ts @@ -0,0 +1,108 @@ +import { createSelector } from 'reselect'; +import { Action } from 'redux'; // Import types for actions +import * as actionConstants from '../../store/actionConstants'; +import { FALLBACK_LOCALE } from '../../../shared/modules/i18n'; + +/** + * Type for the locale messages part of the state + */ +type LocaleMessagesState = { + current?: { [key: string]: string }; // Messages for the current locale + currentLocale?: string; // User's selected locale (unsafe for Intl API) + en?: { [key: string]: string }; // English locale messages +}; + +/** + * Payload for the SET_CURRENT_LOCALE action + */ +type SetCurrentLocaleAction = Action & { + type: typeof actionConstants.SET_CURRENT_LOCALE; + payload: { + messages: { [key: string]: string }; + locale: string; + }; +}; + +/** + * Type for actions that can be handled by reduceLocaleMessages + */ +type LocaleMessagesActions = SetCurrentLocaleAction; + +/** + * Initial state for localeMessages reducer + */ +const initialState: LocaleMessagesState = {}; + +/** + * Reducer for localeMessages + * + * @param state - The current state + * @param action - The action being dispatched + * @returns The updated locale messages state + */ +export default function reduceLocaleMessages( + // eslint-disable-next-line @typescript-eslint/default-param-last + state: LocaleMessagesState = initialState, + action: LocaleMessagesActions, +): LocaleMessagesState { + switch (action.type) { + case actionConstants.SET_CURRENT_LOCALE: + return { + ...state, + current: action.payload.messages, + currentLocale: action.payload.locale, + }; + default: + return state; + } +} + +/** + * Type for the overall Redux state + */ +type AppState = { + localeMessages: LocaleMessagesState; +}; + +/** + * This selector returns a code from file://./../../../app/_locales/index.json. + * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. + * + * @param state - The overall state + * @returns The user's selected locale + */ +export const getCurrentLocale = (state: AppState): string | undefined => + state.localeMessages.currentLocale; + +/** + * This selector returns a BCP 47 Language Tag for use with the Intl API. + * + * @returns The user's selected locale in BCP 47 format + */ +export const getIntlLocale = createSelector( + getCurrentLocale, + (locale): string => + Intl.getCanonicalLocales( + locale ? locale.replace(/_/gu, '-') : FALLBACK_LOCALE, + )[0], +); + +/** + * This selector returns the current locale messages. + * + * @param state - The overall state + * @returns The current locale's messages + */ +export const getCurrentLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.current; + +/** + * This selector returns the English locale messages. + * + * @param state - The overall state + * @returns The English locale's messages + */ +export const getEnLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.en; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 05cc6d46cb27..63ff92a11ccc 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -17,9 +17,9 @@ import { checkNetworkAndAccountSupports1559, getAddressBook, getSelectedNetworkClientId, - getSelectedInternalAccount, getNetworkConfigurationsByChainId, -} from '../../selectors'; +} from '../../selectors/selectors'; +import { getSelectedInternalAccount } from '../../selectors/accounts'; import * as actionConstants from '../../store/actionConstants'; import { updateTransactionGasFees } from '../../store/actions'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; @@ -47,9 +47,10 @@ const initialState = { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, petnamesEnabled: true, featureNotificationsEnabled: false, + privacyMode: false, showMultiRpcModal: false, }, firstTimeFlowType: null, diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index 3cd543a65219..8bd6865295d8 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -205,7 +205,7 @@ describe('rampsSlice', () => { }); it('should return true when Bitcoin is buyable and current chain is Bitcoin', () => { - getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN); + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); getMultichainIsBitcoinMock.mockReturnValue(true); const mockBuyableChains = [ { chainId: MultichainNetworks.BITCOIN, active: true }, @@ -219,7 +219,7 @@ describe('rampsSlice', () => { }); it('should return false when Bitcoin is not buyable and current chain is Bitcoin', () => { - getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN); + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); getMultichainIsBitcoinMock.mockReturnValue(true); const mockBuyableChains = [ { chainId: MultichainNetworks.BITCOIN, active: false }, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 512b621c38f4..30cbc6eeb5dd 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -2615,7 +2615,12 @@ export function updateSendAsset( let missingProperty = STANDARD_TO_REQUIRED_PROPERTIES[ providedDetails.standard - ]?.find((property) => providedDetails[property] === undefined); + ]?.find((property) => { + if (providedDetails.collection && property === 'symbol') { + return providedDetails.collection[property] === undefined; + } + return providedDetails[property] === undefined; + }); let details; @@ -2652,10 +2657,9 @@ export function updateSendAsset( providedDetails.address, sendingAddress, providedDetails.tokenId, - ).catch((error) => { + ).catch(() => { // prevent infinite stuck loading state dispatch(hideLoadingIndication()); - throw error; })), }; } diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 8dd7336d7a62..d23c0ce69381 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -858,6 +858,7 @@ export const fetchQuotesAndSetQuoteState = ( stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), + gas_included: newSelectedQuote.isGasIncludedTrade, anonymizedData: true, }, }); diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index c22b0cbcf183..232bcdae5aff 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,4 +1,8 @@ /* eslint-disable @metamask/design-tokens/color-no-hex*/ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../shared/constants/app'; import { IconName } from '../../components/component-library'; import { ADVANCED_ROUTE, @@ -19,6 +23,7 @@ import { * # @param {string} route tab route with appended arbitrary, unique anchor tag / hash route * # @param {string} iconName * # @param {string} featureFlag ENV variable name. If the ENV value exists, the route will be searchable; else, route will not be searchable. + * # @param {boolean} hidden If true, the route will not be searchable. */ /** @type {SettingRouteConfig[]} */ @@ -154,6 +159,16 @@ const SETTINGS_CONSTANTS = [ route: `${ADVANCED_ROUTE}#export-data`, icon: 'fas fa-download', }, + // advanced settingsRefs[11] + { + tabMessage: (t) => t('advanced'), + sectionMessage: (t) => t('overrideContentSecurityPolicyHeader'), + descriptionMessage: (t) => + t('overrideContentSecurityPolicyHeaderDescription'), + route: `${ADVANCED_ROUTE}#override-content-security-policy-header`, + icon: 'fas fa-sliders-h', + hidden: getPlatform() !== PLATFORM_FIREFOX, + }, { tabMessage: (t) => t('contacts'), sectionMessage: (t) => t('contacts'), diff --git a/ui/helpers/utils/notification.util.ts b/ui/helpers/utils/notification.util.ts index 489f1ca2f272..afbba2b88172 100644 --- a/ui/helpers/utils/notification.util.ts +++ b/ui/helpers/utils/notification.util.ts @@ -420,9 +420,11 @@ export const getNetworkFees = async (notification: OnChainRawNotification) => { const rpcUrl = getRpcUrlByChainId(`0x${chainId}` as HexChainId); const connection = { url: rpcUrl, - headers: { - 'Infura-Source': 'metamask/metamask', - }, + headers: process.env.STORYBOOK + ? undefined + : { + 'Infura-Source': 'metamask/metamask', + }, }; const provider = new JsonRpcProvider(connection); diff --git a/ui/helpers/utils/notification.utils.test.ts b/ui/helpers/utils/notification.utils.test.ts index f82f8532cb58..2f4e66c26504 100644 --- a/ui/helpers/utils/notification.utils.test.ts +++ b/ui/helpers/utils/notification.utils.test.ts @@ -9,7 +9,7 @@ import { describe('formatMenuItemDate', () => { beforeAll(() => { jest.useFakeTimers(); - jest.setSystemTime(new Date('2024-06-07T09:40:00Z')); + jest.setSystemTime(new Date(Date.UTC(2024, 5, 7, 9, 40, 0))); // 2024-06-07T09:40:00Z }); afterAll(() => { @@ -28,7 +28,7 @@ describe('formatMenuItemDate', () => { // assert 1 hour ago assertToday((testDate) => { - testDate.setHours(testDate.getHours() - 1); + testDate.setUTCHours(testDate.getUTCHours() - 1); return testDate; }); }); @@ -42,14 +42,14 @@ describe('formatMenuItemDate', () => { // assert exactly 1 day ago assertYesterday((testDate) => { - testDate.setDate(testDate.getDate() - 1); + testDate.setUTCDate(testDate.getUTCDate() - 1); }); // assert almost a day ago, but was still yesterday // E.g. if Today way 09:40AM, but date to test was 23 hours ago (yesterday at 10:40AM), we still want to to show yesterday assertYesterday((testDate) => { - testDate.setDate(testDate.getDate() - 1); - testDate.setHours(testDate.getHours() + 1); + testDate.setUTCDate(testDate.getUTCDate() - 1); + testDate.setUTCHours(testDate.getUTCHours() + 1); }); }); @@ -62,18 +62,18 @@ describe('formatMenuItemDate', () => { // assert exactly 1 month ago assertMonthsAgo((testDate) => { - testDate.setMonth(testDate.getMonth() - 1); + testDate.setUTCMonth(testDate.getUTCMonth() - 1); }); // assert 2 months ago assertMonthsAgo((testDate) => { - testDate.setMonth(testDate.getMonth() - 2); + testDate.setUTCMonth(testDate.getUTCMonth() - 2); }); // assert almost a month ago (where it is a new month, but not 30 days) assertMonthsAgo(() => { // jest mock date is set in july, so we will test with month may - return new Date('2024-05-20T09:40:00Z'); + return new Date(Date.UTC(2024, 4, 20, 9, 40, 0)); // 2024-05-20T09:40:00Z }); }); @@ -86,18 +86,18 @@ describe('formatMenuItemDate', () => { // assert exactly 1 year ago assertYearsAgo((testDate) => { - testDate.setFullYear(testDate.getFullYear() - 1); + testDate.setUTCFullYear(testDate.getUTCFullYear() - 1); }); // assert 2 years ago assertYearsAgo((testDate) => { - testDate.setFullYear(testDate.getFullYear() - 2); + testDate.setUTCFullYear(testDate.getUTCFullYear() - 2); }); // assert almost a year ago (where it is a new year, but not 365 days ago) assertYearsAgo(() => { // jest mock date is set in 2024, so we will test with year 2023 - return new Date('2023-11-20T09:40:00Z'); + return new Date(Date.UTC(2023, 10, 20, 9, 40, 0)); // 2023-11-20T09:40:00Z }); }); }); diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 07b4501c0208..8c11ad8fad52 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -8,8 +8,10 @@ export function getSettingsRoutes() { if (settingsRoutes) { return settingsRoutes; } - settingsRoutes = SETTINGS_CONSTANTS.filter((routeObject) => - routeObject.featureFlag ? process.env[routeObject.featureFlag] : true, + settingsRoutes = SETTINGS_CONSTANTS.filter( + (routeObject) => + (routeObject.featureFlag ? process.env[routeObject.featureFlag] : true) && + !routeObject.hidden, ); return settingsRoutes; } diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index cc7b875d8c5e..30af3ee6b9da 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -68,6 +68,10 @@ const t = (key) => { return 'Dismiss Secret Recovery Phrase backup reminder'; case 'dismissReminderDescriptionField': return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds'; + case 'overrideContentSecurityPolicyHeader': + return 'Override Content-Security-Policy header'; + case 'overrideContentSecurityPolicyHeaderDescription': + return "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility."; case 'Contacts': return 'Contacts'; case 'securityAndPrivacy': @@ -147,9 +151,12 @@ describe('Settings Search Utils', () => { describe('getSettingsRoutes', () => { it('should be an array of settings routes objects', () => { const NUM_OF_ENV_FEATURE_FLAG_SETTINGS = 4; + const NUM_OF_HIDDEN_SETTINGS = 1; expect(getSettingsRoutes()).toHaveLength( - SETTINGS_CONSTANTS.length - NUM_OF_ENV_FEATURE_FLAG_SETTINGS, + SETTINGS_CONSTANTS.length - + NUM_OF_ENV_FEATURE_FLAG_SETTINGS - + NUM_OF_HIDDEN_SETTINGS, ); }); }); diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index a68aeb361bdd..fe7a21e2206f 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -43,6 +43,7 @@ const useBridging = () => { const isMarketingEnabled = useSelector(getDataCollectionForMarketing); const providerConfig = useSelector(getProviderConfig); const keyring = useSelector(getCurrentKeyring); + // @ts-expect-error keyring type is wrong maybe? const usingHardwareWallet = isHardwareKeyring(keyring.type); const isBridgeSupported = useSelector(getIsBridgeEnabled); diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts new file mode 100644 index 000000000000..14ac21d725fd --- /dev/null +++ b/ui/hooks/bridge/useCountdownTimer.test.ts @@ -0,0 +1,34 @@ +import { renderHookWithProvider } from '../../../test/lib/render-helpers'; +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { flushPromises } from '../../../test/lib/timer-helpers'; +import { useCountdownTimer } from './useCountdownTimer'; + +jest.useFakeTimers(); +const renderUseCountdownTimer = (mockStoreState: object) => + renderHookWithProvider(() => useCountdownTimer(), mockStoreState); + +describe('useCountdownTimer', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + it('returns time remaining', async () => { + const quotesLastFetched = Date.now(); + const { result } = renderUseCountdownTimer( + createBridgeMockStore({}, {}, { quotesLastFetched }), + ); + + let i = 0; + while (i <= 30) { + const secondsLeft = Math.min(30, 30 - i + 1); + expect(result.current).toStrictEqual( + `0:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`, + ); + i += 10; + jest.advanceTimersByTime(10000); + await flushPromises(); + } + expect(result.current).toStrictEqual('0:00'); + }); +}); diff --git a/ui/hooks/bridge/useCountdownTimer.ts b/ui/hooks/bridge/useCountdownTimer.ts new file mode 100644 index 000000000000..112d35b33f6a --- /dev/null +++ b/ui/hooks/bridge/useCountdownTimer.ts @@ -0,0 +1,37 @@ +import { Duration } from 'luxon'; +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { getBridgeQuotes } from '../../ducks/bridge/selectors'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; +import { SECOND } from '../../../shared/constants/time'; + +/** + * Custom hook that provides a countdown timer based on the last fetched quotes timestamp. + * + * This hook calculates the remaining time until the next refresh interval and updates every second. + * + * @returns The formatted remaining time in 'm:ss' format. + */ +export const useCountdownTimer = () => { + const [timeRemaining, setTimeRemaining] = useState(REFRESH_INTERVAL_MS); + const { quotesLastFetchedMs } = useSelector(getBridgeQuotes); + + useEffect(() => { + if (quotesLastFetchedMs) { + setTimeRemaining( + REFRESH_INTERVAL_MS - (Date.now() - quotesLastFetchedMs), + ); + } + }, [quotesLastFetchedMs]); + + useEffect(() => { + const interval = setInterval(() => { + setTimeRemaining(Math.max(0, timeRemaining - SECOND)); + }, SECOND); + return () => clearInterval(interval); + }, [timeRemaining]); + + return Duration.fromMillis(timeRemaining).toFormat('m:ss'); +}; diff --git a/ui/hooks/metamask-notifications/useNotifications.ts b/ui/hooks/metamask-notifications/useNotifications.ts index 62367cdbe310..9724253a8671 100644 --- a/ui/hooks/metamask-notifications/useNotifications.ts +++ b/ui/hooks/metamask-notifications/useNotifications.ts @@ -54,8 +54,13 @@ export function useListNotifications(): { setLoading(true); setError(null); + const urlParams = new URLSearchParams(window.location.search); + const previewToken = urlParams.get('previewToken'); + try { - const data = await dispatch(fetchAndUpdateMetamaskNotifications()); + const data = await dispatch( + fetchAndUpdateMetamaskNotifications(previewToken ?? undefined), + ); setNotificationsData(data as unknown as Notification[]); return data as unknown as Notification[]; } catch (e) { diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx deleted file mode 100644 index 951cec333ade..000000000000 --- a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { renderHook, act } from '@testing-library/react-hooks'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { waitFor } from '@testing-library/react'; -import * as actions from '../../store/actions'; -import { - useEnableProfileSyncing, - useDisableProfileSyncing, - useAccountSyncingEffect, - useDeleteAccountSyncingDataFromUserStorage, -} from './useProfileSyncing'; - -const middlewares = [thunk]; -const mockStore = configureStore(middlewares); - -jest.mock('../../store/actions', () => ({ - performSignIn: jest.fn(), - performSignOut: jest.fn(), - enableProfileSyncing: jest.fn(), - disableProfileSyncing: jest.fn(), - showLoadingIndication: jest.fn(), - hideLoadingIndication: jest.fn(), - syncInternalAccountsWithUserStorage: jest.fn(), - deleteAccountSyncingDataFromUserStorage: jest.fn(), -})); - -type ArrangeMocksMetamaskStateOverrides = { - isSignedIn?: boolean; - isProfileSyncingEnabled?: boolean; - isUnlocked?: boolean; - useExternalServices?: boolean; - completedOnboarding?: boolean; -}; - -const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = { - isSignedIn: false, - isProfileSyncingEnabled: false, - isUnlocked: true, - useExternalServices: true, - completedOnboarding: true, -}; - -const arrangeMocks = ( - metamaskStateOverrides?: ArrangeMocksMetamaskStateOverrides, -) => { - const store = mockStore({ - metamask: { - ...initialMetamaskState, - ...metamaskStateOverrides, - participateInMetaMetrics: false, - internalAccounts: { - accounts: { - '0x123': { - address: '0x123', - id: 'account1', - metadata: {}, - options: {}, - methods: [], - type: 'eip155:eoa', - }, - }, - }, - }, - }); - - store.dispatch = jest.fn().mockImplementation((action) => { - if (typeof action === 'function') { - return action(store.dispatch, store.getState); - } - return Promise.resolve(); - }); - - jest.clearAllMocks(); - - return { store }; -}; - -describe('useProfileSyncing', () => { - it('should enable profile syncing', async () => { - const { store } = arrangeMocks(); - - const { result } = renderHook(() => useEnableProfileSyncing(), { - wrapper: ({ children }) => {children}, - }); - - act(() => { - result.current.enableProfileSyncing(); - }); - - expect(actions.enableProfileSyncing).toHaveBeenCalled(); - }); - - it('should disable profile syncing', async () => { - const { store } = arrangeMocks(); - - const { result } = renderHook(() => useDisableProfileSyncing(), { - wrapper: ({ children }) => {children}, - }); - - act(() => { - result.current.disableProfileSyncing(); - }); - - expect(actions.disableProfileSyncing).toHaveBeenCalled(); - }); - - it('should dispatch account syncing when conditions are met', async () => { - const { store } = arrangeMocks({ - isSignedIn: true, - isProfileSyncingEnabled: true, - }); - - renderHook(() => useAccountSyncingEffect(), { - wrapper: ({ children }) => {children}, - }); - - await waitFor(() => { - expect(actions.syncInternalAccountsWithUserStorage).toHaveBeenCalled(); - }); - }); - - it('should not dispatch account syncing when conditions are not met', async () => { - const { store } = arrangeMocks(); - - renderHook(() => useAccountSyncingEffect(), { - wrapper: ({ children }) => {children}, - }); - - await waitFor(() => { - expect( - actions.syncInternalAccountsWithUserStorage, - ).not.toHaveBeenCalled(); - }); - }); - - it('should dispatch account sync data deletion', async () => { - const { store } = arrangeMocks(); - - const { result } = renderHook( - () => useDeleteAccountSyncingDataFromUserStorage(), - { - wrapper: ({ children }) => ( - {children} - ), - }, - ); - - act(() => { - result.current.dispatchDeleteAccountSyncingDataFromUserStorage(); - }); - - expect(actions.deleteAccountSyncingDataFromUserStorage).toHaveBeenCalled(); - }); -}); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx new file mode 100644 index 000000000000..604466b3a75c --- /dev/null +++ b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx @@ -0,0 +1,70 @@ +import { waitFor } from '@testing-library/react'; +import { act } from '@testing-library/react-hooks'; +import { renderHookWithProviderTyped } from '../../../../test/lib/render-helpers'; +import * as actions from '../../../store/actions'; +import { + useAccountSyncingEffect, + useDeleteAccountSyncingDataFromUserStorage, +} from './accountSyncing'; +import * as ProfileSyncModule from './profileSyncing'; + +describe('useDeleteAccountSyncingDataFromUserStorage()', () => { + it('should dispatch account sync data deletion', async () => { + const mockDeleteAccountSyncAction = jest.spyOn( + actions, + 'deleteAccountSyncingDataFromUserStorage', + ); + + const { result } = renderHookWithProviderTyped( + () => useDeleteAccountSyncingDataFromUserStorage(), + {}, + ); + + await act(async () => { + await result.current.dispatchDeleteAccountData(); + }); + + expect(mockDeleteAccountSyncAction).toHaveBeenCalled(); + }); +}); + +describe('useAccountSyncingEffect', () => { + const arrangeMocks = () => { + const mockUseShouldProfileSync = jest.spyOn( + ProfileSyncModule, + 'useShouldDispatchProfileSyncing', + ); + const mockSyncAccountsAction = jest.spyOn( + actions, + 'syncInternalAccountsWithUserStorage', + ); + return { + mockUseShouldProfileSync, + mockSyncAccountsAction, + }; + }; + + const arrangeAndAct = (props: { profileSyncConditionsMet: boolean }) => { + const mocks = arrangeMocks(); + mocks.mockUseShouldProfileSync.mockReturnValue( + props.profileSyncConditionsMet, + ); + + renderHookWithProviderTyped(() => useAccountSyncingEffect(), {}); + return mocks; + }; + + it('should run effect if profile sync conditions are met', async () => { + const mocks = arrangeAndAct({ profileSyncConditionsMet: true }); + await waitFor(() => { + expect(mocks.mockSyncAccountsAction).toHaveBeenCalled(); + }); + }); + + it('should not run effect if profile sync conditions are not met', async () => { + const mocks = arrangeAndAct({ profileSyncConditionsMet: false }); + await waitFor(() => { + expect(mocks.mockSyncAccountsAction).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts new file mode 100644 index 000000000000..cef4dc80fa75 --- /dev/null +++ b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts @@ -0,0 +1,66 @@ +import log from 'loglevel'; +import { useCallback, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { + deleteAccountSyncingDataFromUserStorage, + syncInternalAccountsWithUserStorage, +} from '../../../store/actions'; +import { useShouldDispatchProfileSyncing } from './profileSyncing'; + +/** + * Custom hook to dispatch account syncing. + * + * @returns An object containing the `dispatchAccountSyncing` function, boolean `shouldDispatchAccountSyncing`, + * and error state. + */ +const useAccountSyncing = () => { + const dispatch = useDispatch(); + + const shouldDispatchAccountSyncing = useShouldDispatchProfileSyncing(); + + const dispatchAccountSyncing = useCallback(() => { + try { + if (!shouldDispatchAccountSyncing) { + return; + } + dispatch(syncInternalAccountsWithUserStorage()); + } catch (e) { + log.error(e); + } + }, [dispatch, shouldDispatchAccountSyncing]); + + return { + dispatchAccountSyncing, + shouldDispatchAccountSyncing, + }; +}; + +/** + * Custom hook to apply account syncing effect. + */ +export const useAccountSyncingEffect = () => { + const shouldSync = useShouldDispatchProfileSyncing(); + const { dispatchAccountSyncing } = useAccountSyncing(); + + useEffect(() => { + if (shouldSync) { + dispatchAccountSyncing(); + } + }, [shouldSync, dispatchAccountSyncing]); +}; + +/** + * Custom hook to delete a user's account syncing data from user storage + */ +export const useDeleteAccountSyncingDataFromUserStorage = () => { + const dispatch = useDispatch(); + const dispatchDeleteAccountData = useCallback(async () => { + try { + await dispatch(deleteAccountSyncingDataFromUserStorage()); + } catch { + // Do Nothing + } + }, []); + + return { dispatchDeleteAccountData }; +}; diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/index.ts b/ui/hooks/metamask-notifications/useProfileSyncing/index.ts new file mode 100644 index 000000000000..9a6cda8468fb --- /dev/null +++ b/ui/hooks/metamask-notifications/useProfileSyncing/index.ts @@ -0,0 +1,9 @@ +export { + useDisableProfileSyncing, + useEnableProfileSyncing, + useSetIsProfileSyncingEnabled, +} from './profileSyncing'; +export { + useAccountSyncingEffect, + useDeleteAccountSyncingDataFromUserStorage, +} from './accountSyncing'; diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx new file mode 100644 index 000000000000..99d3064085ea --- /dev/null +++ b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx @@ -0,0 +1,136 @@ +import { act } from '@testing-library/react-hooks'; +import { renderHookWithProviderTyped } from '../../../../test/lib/render-helpers'; +import { MetamaskNotificationsProvider } from '../../../contexts/metamask-notifications'; +import * as actions from '../../../store/actions'; +import { + useDisableProfileSyncing, + useEnableProfileSyncing, + useShouldDispatchProfileSyncing, +} from './profileSyncing'; + +type ArrangeMocksMetamaskStateOverrides = { + isSignedIn?: boolean; + isProfileSyncingEnabled?: boolean; + isUnlocked?: boolean; + useExternalServices?: boolean; + completedOnboarding?: boolean; +}; + +const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = { + isSignedIn: false, + isProfileSyncingEnabled: false, + isUnlocked: true, + useExternalServices: true, + completedOnboarding: true, +}; + +const arrangeMockState = ( + metamaskStateOverrides?: ArrangeMocksMetamaskStateOverrides, +) => { + const state = { + metamask: { + ...initialMetamaskState, + ...metamaskStateOverrides, + }, + }; + + return { state }; +}; + +describe('useEnableProfileSyncing()', () => { + it('should enable profile syncing', async () => { + const mockEnableProfileSyncingAction = jest.spyOn( + actions, + 'enableProfileSyncing', + ); + + const { state } = arrangeMockState(); + const { result } = renderHookWithProviderTyped( + () => useEnableProfileSyncing(), + state, + ); + await act(async () => { + await result.current.enableProfileSyncing(); + }); + + expect(mockEnableProfileSyncingAction).toHaveBeenCalled(); + }); +}); + +describe('useDisableProfileSyncing()', () => { + it('should disable profile syncing', async () => { + const mockDisableProfileSyncingAction = jest.spyOn( + actions, + 'disableProfileSyncing', + ); + + const { state } = arrangeMockState(); + + const { result } = renderHookWithProviderTyped( + () => useDisableProfileSyncing(), + state, + undefined, + MetamaskNotificationsProvider, + ); + + await act(async () => { + await result.current.disableProfileSyncing(); + }); + + expect(mockDisableProfileSyncingAction).toHaveBeenCalled(); + }); +}); + +describe('useShouldDispatchProfileSyncing()', () => { + const testCases = (() => { + const properties = [ + 'isSignedIn', + 'isProfileSyncingEnabled', + 'isUnlocked', + 'useExternalServices', + 'completedOnboarding', + ] as const; + const baseState = { + isSignedIn: true, + isProfileSyncingEnabled: true, + isUnlocked: true, + useExternalServices: true, + completedOnboarding: true, + }; + + const failureStateCases: { + state: ArrangeMocksMetamaskStateOverrides; + failingField: string; + }[] = []; + + // Generate test cases by toggling each property + properties.forEach((property) => { + const state = { ...baseState, [property]: false }; + failureStateCases.push({ state, failingField: property }); + }); + + const successTestCase = { state: baseState }; + + return { successTestCase, failureStateCases }; + })(); + + it('should return true if all conditions are met', () => { + const { state } = arrangeMockState(testCases.successTestCase.state); + const hook = renderHookWithProviderTyped( + () => useShouldDispatchProfileSyncing(), + state, + ); + expect(hook.result.current).toBe(true); + }); + + testCases.failureStateCases.forEach(({ state, failingField }) => { + it(`should return false if not all conditions are met [${failingField} = false]`, () => { + const { state: newState } = arrangeMockState(state); + const hook = renderHookWithProviderTyped( + () => useShouldDispatchProfileSyncing(), + newState, + ); + expect(hook.result.current).toBe(false); + }); + }); +}); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts similarity index 53% rename from ui/hooks/metamask-notifications/useProfileSyncing.ts rename to ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts index 67899aa73927..5c073fdf6d94 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing.ts +++ b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts @@ -1,35 +1,21 @@ -import { useState, useCallback, useEffect, useMemo } from 'react'; +import { useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import type { InternalAccount } from '@metamask/keyring-api'; import log from 'loglevel'; +import { useMetamaskNotificationsContext } from '../../../contexts/metamask-notifications/metamask-notifications'; import { disableProfileSyncing as disableProfileSyncingAction, enableProfileSyncing as enableProfileSyncingAction, setIsProfileSyncingEnabled as setIsProfileSyncingEnabledAction, hideLoadingIndication, - syncInternalAccountsWithUserStorage, - deleteAccountSyncingDataFromUserStorage, -} from '../../store/actions'; +} from '../../../store/actions'; -import { selectIsSignedIn } from '../../selectors/metamask-notifications/authentication'; -import { selectIsProfileSyncingEnabled } from '../../selectors/metamask-notifications/profile-syncing'; -import { getUseExternalServices } from '../../selectors'; +import { selectIsSignedIn } from '../../../selectors/metamask-notifications/authentication'; +import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; +import { getUseExternalServices } from '../../../selectors'; import { getIsUnlocked, getCompletedOnboarding, -} from '../../ducks/metamask/metamask'; - -// Define KeyringType interface -export type KeyringType = { - type: string; -}; - -// Define AccountType interface -export type AccountType = InternalAccount & { - balance: string; - keyring: KeyringType; - label: string; -}; +} from '../../../ducks/metamask/metamask'; /** * Custom hook to enable profile syncing. This hook handles the process of signing in @@ -74,6 +60,7 @@ export function useDisableProfileSyncing(): { error: string | null; } { const dispatch = useDispatch(); + const { listNotifications } = useMetamaskNotificationsContext(); const [error, setError] = useState(null); @@ -83,6 +70,9 @@ export function useDisableProfileSyncing(): { try { // disable profile syncing await dispatch(disableProfileSyncingAction()); + + // list notifications to update the counter + await listNotifications(); } catch (e) { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e ?? ''); @@ -124,92 +114,29 @@ export function useSetIsProfileSyncingEnabled(): { } /** - * Custom hook to dispatch account syncing. + * A utility used internally to decide if syncing features should be dispatched + * Considers factors like basic functionality; unlocked; finished onboarding, and is logged in * - * @returns An object containing the `dispatchAccountSyncing` function, boolean `shouldDispatchAccountSyncing`, - * and error state. + * @returns a boolean if internally we can perform syncing features or not. */ -export const useAccountSyncing = () => { - const dispatch = useDispatch(); - - const [error, setError] = useState(null); - +export const useShouldDispatchProfileSyncing = () => { const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); - const basicFunctionality = useSelector(getUseExternalServices); - const isUnlocked = useSelector(getIsUnlocked); + const basicFunctionality: boolean | undefined = useSelector( + getUseExternalServices, + ); + const isUnlocked: boolean | undefined = useSelector(getIsUnlocked); const isSignedIn = useSelector(selectIsSignedIn); - const completedOnboarding = useSelector(getCompletedOnboarding); + const completedOnboarding: boolean | undefined = useSelector( + getCompletedOnboarding, + ); - const shouldDispatchAccountSyncing = useMemo( - () => - basicFunctionality && + const shouldDispatchProfileSyncing: boolean = Boolean( + basicFunctionality && isProfileSyncingEnabled && isUnlocked && isSignedIn && completedOnboarding, - [ - basicFunctionality, - isProfileSyncingEnabled, - isUnlocked, - isSignedIn, - completedOnboarding, - ], ); - const dispatchAccountSyncing = useCallback(() => { - setError(null); - - try { - if (!shouldDispatchAccountSyncing) { - return; - } - dispatch(syncInternalAccountsWithUserStorage()); - } catch (e) { - log.error(e); - setError(e instanceof Error ? e.message : 'An unexpected error occurred'); - } - }, [dispatch, shouldDispatchAccountSyncing]); - - return { - dispatchAccountSyncing, - shouldDispatchAccountSyncing, - error, - }; -}; - -/** - * Custom hook to delete a user's account syncing data from user storage - */ - -export const useDeleteAccountSyncingDataFromUserStorage = () => { - const dispatch = useDispatch(); - - const [error, setError] = useState(null); - - const dispatchDeleteAccountSyncingDataFromUserStorage = useCallback(() => { - setError(null); - - try { - dispatch(deleteAccountSyncingDataFromUserStorage()); - } catch (e) { - log.error(e); - setError(e instanceof Error ? e.message : 'An unexpected error occurred'); - } - }, [dispatch]); - - return { - dispatchDeleteAccountSyncingDataFromUserStorage, - error, - }; -}; - -/** - * Custom hook to apply account syncing effect. - */ -export const useAccountSyncingEffect = () => { - const { dispatchAccountSyncing } = useAccountSyncing(); - - useEffect(() => { - dispatchAccountSyncing(); - }, [dispatchAccountSyncing]); + return shouldDispatchProfileSyncing; }; diff --git a/ui/hooks/snaps/useDisplayName.ts b/ui/hooks/snaps/useDisplayName.ts new file mode 100644 index 000000000000..6a6d3d7e6b51 --- /dev/null +++ b/ui/hooks/snaps/useDisplayName.ts @@ -0,0 +1,54 @@ +import { NamespaceId } from '@metamask/snaps-utils'; +import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; +import { useSelector } from 'react-redux'; +import { + getMemoizedAccountName, + getAddressBookEntryByNetwork, + AddressBookMetaMaskState, + AccountsMetaMaskState, +} from '../../selectors/snaps'; +import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; +import { decimalToHex } from '../../../shared/modules/conversion.utils'; + +export type UseDisplayNameParams = { + chain: { + namespace: NamespaceId; + reference: string; + }; + chainId: CaipChainId; + address: string; +}; + +/** + * Get the display name for an address. + * This will look for an account name in the state, and if not found, it will look for an address book entry. + * + * @param params - The parsed CAIP-10 ID. + * @returns The display name for the address. + */ +export const useDisplayName = ( + params: UseDisplayNameParams, +): string | undefined => { + const { + address, + chain: { namespace, reference }, + } = params; + + const isEip155 = namespace === KnownCaipNamespace.Eip155; + + const parsedAddress = isEip155 ? toChecksumHexAddress(address) : address; + + const accountName = useSelector((state: AccountsMetaMaskState) => + getMemoizedAccountName(state, parsedAddress), + ); + + const addressBookEntry = useSelector((state: AddressBookMetaMaskState) => + getAddressBookEntryByNetwork( + state, + parsedAddress, + `0x${decimalToHex(isEip155 ? reference : `0`)}`, + ), + ); + + return accountName || (isEip155 && addressBookEntry?.name) || undefined; +}; diff --git a/ui/hooks/useAccountTotalFiatBalance.js b/ui/hooks/useAccountTotalFiatBalance.js index b0c9b293c906..7b4a4675225a 100644 --- a/ui/hooks/useAccountTotalFiatBalance.js +++ b/ui/hooks/useAccountTotalFiatBalance.js @@ -51,7 +51,6 @@ export const useAccountTotalFiatBalance = ( const tokens = detectedTokens?.[currentChainId]?.[account?.address] ?? []; // This selector returns all the tokens, we need it to get the image of token const allTokenList = useSelector(getTokenList); - const allTokenListValues = Object.values(allTokenList); const primaryTokenImage = useSelector(getNativeCurrencyImage); const nativeCurrency = useSelector(getNativeCurrency); @@ -92,20 +91,18 @@ export const useAccountTotalFiatBalance = ( }; // To match the list of detected tokens with the entire token list to find the image for tokens - const findMatchingTokens = (array1, array2) => { + const findMatchingTokens = (tokenList, _tokensWithBalances) => { const result = []; - array2.forEach((token2) => { - const matchingToken = array1.find( - (token1) => token1.symbol === token2.symbol, - ); + _tokensWithBalances.forEach((token) => { + const matchingToken = tokenList[token.address.toLowerCase()]; if (matchingToken) { result.push({ ...matchingToken, - balance: token2.balance, - string: token2.string, - balanceError: token2.balanceError, + balance: token.balance, + string: token.string, + balanceError: token.balanceError, }); } }); @@ -113,10 +110,7 @@ export const useAccountTotalFiatBalance = ( return result; }; - const matchingTokens = findMatchingTokens( - allTokenListValues, - tokensWithBalances, - ); + const matchingTokens = findMatchingTokens(allTokenList, tokensWithBalances); // Combine native token, detected token with image in an array const allTokensWithFiatValues = [ diff --git a/ui/hooks/useCurrencyRatePolling.ts b/ui/hooks/useCurrencyRatePolling.ts index fb14b1c94797..e7ad21adedf5 100644 --- a/ui/hooks/useCurrencyRatePolling.ts +++ b/ui/hooks/useCurrencyRatePolling.ts @@ -1,24 +1,30 @@ import { useSelector } from 'react-redux'; import { - getSelectedNetworkClientId, + getNetworkConfigurationsByChainId, getUseCurrencyRateCheck, } from '../selectors'; import { - currencyRateStartPollingByNetworkClientId, + currencyRateStartPolling, currencyRateStopPollingByPollingToken, } from '../store/actions'; import { getCompletedOnboarding } from '../ducks/metamask/metamask'; import usePolling from './usePolling'; -const useCurrencyRatePolling = (networkClientId?: string) => { +const useCurrencyRatePolling = () => { const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const completedOnboarding = useSelector(getCompletedOnboarding); - const selectedNetworkClientId = useSelector(getSelectedNetworkClientId); + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + + const nativeCurrencies = [ + ...new Set( + Object.values(networkConfigurations).map((n) => n.nativeCurrency), + ), + ]; usePolling({ - startPollingByNetworkClientId: currencyRateStartPollingByNetworkClientId, + startPolling: currencyRateStartPolling, stopPollingByPollingToken: currencyRateStopPollingByPollingToken, - networkClientId: networkClientId ?? selectedNetworkClientId, + input: nativeCurrencies, enabled: useCurrencyRateCheck && completedOnboarding, }); }; diff --git a/ui/hooks/useDisplayName.test.ts b/ui/hooks/useDisplayName.test.ts index 1d6fb22b5e69..896e974c0025 100644 --- a/ui/hooks/useDisplayName.test.ts +++ b/ui/hooks/useDisplayName.test.ts @@ -1,218 +1,595 @@ -import { NameEntry, NameType } from '@metamask/name-controller'; -import { NftContract } from '@metamask/assets-controllers'; -import { renderHook } from '@testing-library/react-hooks'; -import { getRemoteTokens } from '../selectors'; -import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft'; +import { NameType } from '@metamask/name-controller'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import { + EXPERIENCES_TYPE, + FIRST_PARTY_CONTRACT_NAMES, +} from '../../shared/constants/first-party-contracts'; +import mockState from '../../test/data/mock-state.json'; +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import { getDomainResolutions } from '../ducks/domains'; import { useDisplayName } from './useDisplayName'; import { useNames } from './useName'; -import { useFirstPartyContractNames } from './useFirstPartyContractName'; import { useNftCollectionsMetadata } from './useNftCollectionsMetadata'; -jest.mock('react-redux', () => ({ - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - useSelector: (selector: any) => selector(), -})); - -jest.mock('./useName', () => ({ - useNames: jest.fn(), -})); - -jest.mock('./useFirstPartyContractName', () => ({ - useFirstPartyContractNames: jest.fn(), -})); - -jest.mock('./useNftCollectionsMetadata', () => ({ - useNftCollectionsMetadata: jest.fn(), -})); - -jest.mock('../selectors', () => ({ - getRemoteTokens: jest.fn(), - getCurrentChainId: jest.fn(), +jest.mock('./useName'); +jest.mock('./useNftCollectionsMetadata'); +jest.mock('../ducks/domains', () => ({ + getDomainResolutions: jest.fn(), })); -jest.mock('../selectors/nft', () => ({ - getNftContractsByAddressOnCurrentChain: jest.fn(), -})); - -const VALUE_MOCK = '0xabc123'; -const TYPE_MOCK = NameType.ETHEREUM_ADDRESS; -const NAME_MOCK = 'TestName'; -const CONTRACT_NAME_MOCK = 'TestContractName'; -const FIRST_PARTY_CONTRACT_NAME_MOCK = 'MetaMask Bridge'; -const WATCHED_NFT_NAME_MOCK = 'TestWatchedNFTName'; - -const NO_PETNAME_FOUND_RETURN_VALUE = { - name: null, -} as NameEntry; -const NO_CONTRACT_NAME_FOUND_RETURN_VALUE = undefined; -const NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE = null; -const NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE = {}; - -const PETNAME_FOUND_RETURN_VALUE = { - name: NAME_MOCK, -} as NameEntry; - -const WATCHED_NFT_FOUND_RETURN_VALUE = { - [VALUE_MOCK]: { - name: WATCHED_NFT_NAME_MOCK, - } as NftContract, -}; +const VALUE_MOCK = 'testvalue'; +const VARIATION_MOCK = CHAIN_IDS.GOERLI; +const PETNAME_MOCK = 'testName1'; +const ERC20_TOKEN_NAME_MOCK = 'testName2'; +const WATCHED_NFT_NAME_MOCK = 'testName3'; +const NFT_NAME_MOCK = 'testName4'; +const FIRST_PARTY_CONTRACT_NAME_MOCK = 'testName5'; +const ENS_NAME_MOCK = 'vitalik.eth'; +const SYMBOL_MOCK = 'tes'; +const NFT_IMAGE_MOCK = 'testNftImage'; +const ERC20_IMAGE_MOCK = 'testImage'; +const OTHER_NAME_TYPE = 'test' as NameType; describe('useDisplayName', () => { const useNamesMock = jest.mocked(useNames); - const getRemoteTokensMock = jest.mocked(getRemoteTokens); - const useFirstPartyContractNamesMock = jest.mocked( - useFirstPartyContractNames, - ); - const getNftContractsByAddressOnCurrentChainMock = jest.mocked( - getNftContractsByAddressOnCurrentChain, - ); const useNftCollectionsMetadataMock = jest.mocked(useNftCollectionsMetadata); + const domainResolutionsMock = jest.mocked(getDomainResolutions); - beforeEach(() => { - jest.resetAllMocks(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let state: any; - useNamesMock.mockReturnValue([NO_PETNAME_FOUND_RETURN_VALUE]); - useFirstPartyContractNamesMock.mockReturnValue([ - NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE, - ]); - getRemoteTokensMock.mockReturnValue([ + function mockPetname(name: string) { + useNamesMock.mockReturnValue([ { - name: NO_CONTRACT_NAME_FOUND_RETURN_VALUE, + name, + sourceId: null, + proposedNames: {}, + origin: null, }, ]); - getNftContractsByAddressOnCurrentChainMock.mockReturnValue( - NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE, - ); - useNftCollectionsMetadataMock.mockReturnValue({}); - }); + } + + function mockERC20Token( + value: string, + variation: string, + name: string, + symbol: string, + image: string, + ) { + state.metamask.tokensChainsCache = { + [variation]: { + data: { + [value]: { + name, + symbol, + iconUrl: image, + }, + }, + }, + }; + } - it('handles no name found', () => { - const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK)); - expect(result.current).toEqual({ - name: null, - hasPetname: false, + function mockWatchedNFTName(value: string, variation: string, name: string) { + state.metamask.allNftContracts = { + '0x123': { + [variation]: [{ address: value, name }], + }, + }; + } + + function mockNFT( + value: string, + variation: string, + name: string, + image: string, + isSpam: boolean, + ) { + useNftCollectionsMetadataMock.mockReturnValue({ + [variation]: { + [value]: { name, image, isSpam }, + }, }); - }); + } - it('prioritizes a petname over all else', () => { - useNamesMock.mockReturnValue([PETNAME_FOUND_RETURN_VALUE]); - useFirstPartyContractNamesMock.mockReturnValue([ - FIRST_PARTY_CONTRACT_NAME_MOCK, + function mockDomainResolutions(address: string, ensName: string) { + domainResolutionsMock.mockReturnValue([ + { + addressBookEntryName: undefined, + domainName: ensName, + protocol: 'Ethereum Name Service', + resolvedAddress: address, + resolvingSnap: 'Ethereum Name Service resolver', + }, ]); - getRemoteTokensMock.mockReturnValue([ + } + + function mockFirstPartyContractName( + value: string, + variation: string, + name: string, + ) { + FIRST_PARTY_CONTRACT_NAMES[name as EXPERIENCES_TYPE] = { + [variation as Hex]: value as Hex, + }; + } + + beforeEach(() => { + jest.resetAllMocks(); + + useNftCollectionsMetadataMock.mockReturnValue({}); + + useNamesMock.mockReturnValue([ { - name: CONTRACT_NAME_MOCK, + name: null, + sourceId: null, + proposedNames: {}, + origin: null, }, ]); - getNftContractsByAddressOnCurrentChainMock.mockReturnValue( - WATCHED_NFT_FOUND_RETURN_VALUE, - ); - const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK)); + state = cloneDeep(mockState); - expect(result.current).toEqual({ - name: NAME_MOCK, - hasPetname: true, - contractDisplayName: CONTRACT_NAME_MOCK, - }); + delete FIRST_PARTY_CONTRACT_NAMES[ + FIRST_PARTY_CONTRACT_NAME_MOCK as EXPERIENCES_TYPE + ]; }); - it('prioritizes a first-party contract name over a contract name and watched NFT name', () => { - useFirstPartyContractNamesMock.mockReturnValue([ - FIRST_PARTY_CONTRACT_NAME_MOCK, - ]); - getRemoteTokensMock.mockReturnValue({ - name: CONTRACT_NAME_MOCK, - }); - getNftContractsByAddressOnCurrentChainMock.mockReturnValue( - WATCHED_NFT_FOUND_RETURN_VALUE, + it('returns no name if no defaults found', () => { + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + mockState, ); - const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK)); - - expect(result.current).toEqual({ - name: FIRST_PARTY_CONTRACT_NAME_MOCK, + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, hasPetname: false, + image: undefined, + name: null, }); }); - it('prioritizes a contract name over a watched NFT name', () => { - getRemoteTokensMock.mockReturnValue([ - { - name: CONTRACT_NAME_MOCK, - }, - ]); - getNftContractsByAddressOnCurrentChainMock.mockReturnValue( - WATCHED_NFT_FOUND_RETURN_VALUE, - ); + describe('Petname', () => { + it('returns petname', () => { + mockPetname(PETNAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: true, + image: undefined, + name: PETNAME_MOCK, + }); + }); + }); - const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK)); + describe('ERC-20 Token', () => { + it('returns ERC-20 token name and image', () => { + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: ERC20_TOKEN_NAME_MOCK, + hasPetname: false, + image: ERC20_IMAGE_MOCK, + name: ERC20_TOKEN_NAME_MOCK, + }); + }); - expect(result.current).toEqual({ - name: CONTRACT_NAME_MOCK, - hasPetname: false, - contractDisplayName: CONTRACT_NAME_MOCK, + it('returns ERC-20 token symbol', () => { + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: CHAIN_IDS.GOERLI, + preferContractSymbol: true, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: SYMBOL_MOCK, + hasPetname: false, + image: ERC20_IMAGE_MOCK, + name: SYMBOL_MOCK, + }); + }); + + it('returns no name if type not address', () => { + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: OTHER_NAME_TYPE, + variation: CHAIN_IDS.GOERLI, + preferContractSymbol: true, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: null, + }); }); }); - it('returns a watched NFT name if no other name is found', () => { - getNftContractsByAddressOnCurrentChainMock.mockReturnValue( - WATCHED_NFT_FOUND_RETURN_VALUE, - ); + describe('First-party Contract', () => { + it('returns first-party contract name', () => { + mockFirstPartyContractName( + VALUE_MOCK, + VARIATION_MOCK, + FIRST_PARTY_CONTRACT_NAME_MOCK, + ); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: FIRST_PARTY_CONTRACT_NAME_MOCK, + }); + }); - const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK)); + it('returns no name if type is not address', () => { + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: + FIRST_PARTY_CONTRACT_NAMES[EXPERIENCES_TYPE.METAMASK_BRIDGE][ + CHAIN_IDS.OPTIMISM + ], + type: OTHER_NAME_TYPE, + variation: CHAIN_IDS.OPTIMISM, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: null, + }); + }); + }); - expect(result.current).toEqual({ - name: WATCHED_NFT_NAME_MOCK, - hasPetname: false, + describe('Watched NFT', () => { + it('returns watched NFT name', () => { + mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: WATCHED_NFT_NAME_MOCK, + }); + }); + + it('returns no name if type is not address', () => { + mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: OTHER_NAME_TYPE, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: null, + }); }); }); - it('returns nft collection name from metadata if no other name is found', () => { - const IMAGE_MOCK = 'url'; + describe('NFT', () => { + it('returns NFT name and image', () => { + mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: NFT_IMAGE_MOCK, + name: NFT_NAME_MOCK, + }); + }); - useNftCollectionsMetadataMock.mockReturnValue({ - [VALUE_MOCK.toLowerCase()]: { - name: CONTRACT_NAME_MOCK, - image: IMAGE_MOCK, - isSpam: false, - }, + it('returns no name if NFT collection is spam', () => { + mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, true); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: null, + }); }); - const { result } = renderHook(() => - useDisplayName(VALUE_MOCK, TYPE_MOCK, false), - ); + it('returns no name if type not address', () => { + mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: OTHER_NAME_TYPE, + variation: VARIATION_MOCK, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: null, + }); + }); + }); - expect(result.current).toEqual({ - name: CONTRACT_NAME_MOCK, - hasPetname: false, - contractDisplayName: undefined, - image: IMAGE_MOCK, + describe('Domain Resolutions', () => { + it('returns ENS name if domain resolution for that address exists', () => { + mockDomainResolutions(VALUE_MOCK, ENS_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: ENS_NAME_MOCK, + }); + }); + + it('returns no name if type not address', () => { + mockDomainResolutions(VALUE_MOCK, ENS_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: OTHER_NAME_TYPE, + variation: VARIATION_MOCK, + }), + mockState, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: undefined, + hasPetname: false, + image: undefined, + name: null, + }); }); }); - it('does not return nft collection name if collection is marked as spam', () => { - const IMAGE_MOCK = 'url'; + describe('Priority', () => { + it('uses petname as first priority', () => { + mockPetname(PETNAME_MOCK); + mockFirstPartyContractName( + VALUE_MOCK, + VARIATION_MOCK, + FIRST_PARTY_CONTRACT_NAME_MOCK, + ); + mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false); + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: ERC20_TOKEN_NAME_MOCK, + hasPetname: true, + image: NFT_IMAGE_MOCK, + name: PETNAME_MOCK, + }); + }); - useNftCollectionsMetadataMock.mockReturnValue({ - [VALUE_MOCK.toLowerCase()]: { - name: CONTRACT_NAME_MOCK, - image: IMAGE_MOCK, - isSpam: true, - }, + it('uses first-party contract name as second priority', () => { + mockFirstPartyContractName( + VALUE_MOCK, + VARIATION_MOCK, + FIRST_PARTY_CONTRACT_NAME_MOCK, + ); + mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false); + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: ERC20_TOKEN_NAME_MOCK, + hasPetname: false, + image: NFT_IMAGE_MOCK, + name: FIRST_PARTY_CONTRACT_NAME_MOCK, + }); }); - const { result } = renderHook(() => - useDisplayName(VALUE_MOCK, TYPE_MOCK, false), - ); + it('uses NFT name as third priority', () => { + mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false); + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: ERC20_TOKEN_NAME_MOCK, + hasPetname: false, + image: NFT_IMAGE_MOCK, + name: NFT_NAME_MOCK, + }); + }); - expect(result.current).toEqual( - expect.objectContaining({ - name: null, - image: undefined, - }), - ); + it('uses ERC-20 token name as fourth priority', () => { + mockERC20Token( + VALUE_MOCK, + VARIATION_MOCK, + ERC20_TOKEN_NAME_MOCK, + SYMBOL_MOCK, + ERC20_IMAGE_MOCK, + ); + mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK); + + const { result } = renderHookWithProvider( + () => + useDisplayName({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + variation: VARIATION_MOCK, + }), + state, + ); + + expect(result.current).toStrictEqual({ + contractDisplayName: ERC20_TOKEN_NAME_MOCK, + hasPetname: false, + image: ERC20_IMAGE_MOCK, + name: ERC20_TOKEN_NAME_MOCK, + }); + }); }); }); diff --git a/ui/hooks/useDisplayName.ts b/ui/hooks/useDisplayName.ts index 64a878d2e357..66463b884338 100644 --- a/ui/hooks/useDisplayName.ts +++ b/ui/hooks/useDisplayName.ts @@ -1,16 +1,22 @@ -import { useMemo } from 'react'; import { NameType } from '@metamask/name-controller'; +import { Hex } from '@metamask/utils'; import { useSelector } from 'react-redux'; -import { getRemoteTokens } from '../selectors'; -import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft'; +import { + EXPERIENCES_TYPE, + FIRST_PARTY_CONTRACT_NAMES, +} from '../../shared/constants/first-party-contracts'; +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; +import { getDomainResolutions } from '../ducks/domains'; +import { selectERC20TokensByChain } from '../selectors'; +import { getNftContractsByAddressByChain } from '../selectors/nft'; import { useNames } from './useName'; -import { useFirstPartyContractNames } from './useFirstPartyContractName'; import { useNftCollectionsMetadata } from './useNftCollectionsMetadata'; export type UseDisplayNameRequest = { - value: string; preferContractSymbol?: boolean; type: NameType; + value: string; + variation: string; }; export type UseDisplayNameResponse = { @@ -23,79 +29,176 @@ export type UseDisplayNameResponse = { export function useDisplayNames( requests: UseDisplayNameRequest[], ): UseDisplayNameResponse[] { - const nameRequests = useMemo( - () => requests.map(({ value, type }) => ({ value, type })), - [requests], - ); + const nameEntries = useNames(requests); + const firstPartyContractNames = useFirstPartyContractNames(requests); - const nameEntries = useNames(nameRequests); - const firstPartyContractNames = useFirstPartyContractNames(nameRequests); - const nftCollections = useNftCollectionsMetadata(nameRequests); - const values = requests.map(({ value }) => value); + const erc20Tokens = useERC20Tokens(requests); + const watchedNFTNames = useWatchedNFTNames(requests); + const nfts = useNFTs(requests); + const ens = useDomainResolutions(requests); - const contractInfo = useSelector((state) => - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (getRemoteTokens as any)(state, values), - ); - - const watchedNftNames = useSelector(getNftContractsByAddressOnCurrentChain); - - return requests.map(({ value, preferContractSymbol }, index) => { + return requests.map((_request, index) => { const nameEntry = nameEntries[index]; const firstPartyContractName = firstPartyContractNames[index]; - const singleContractInfo = contractInfo[index]; - const watchedNftName = watchedNftNames[value.toLowerCase()]?.name; - const nftCollectionProperties = nftCollections[value.toLowerCase()]; - - const isNotSpam = nftCollectionProperties?.isSpam === false; - - const nftCollectionName = isNotSpam - ? nftCollectionProperties?.name - : undefined; - const nftCollectionImage = isNotSpam - ? nftCollectionProperties?.image - : undefined; - - const contractDisplayName = - preferContractSymbol && singleContractInfo?.symbol - ? singleContractInfo.symbol - : singleContractInfo?.name; + const erc20Token = erc20Tokens[index]; + const watchedNftName = watchedNFTNames[index]; + const nft = nfts[index]; + const ensName = ens[index]; const name = nameEntry?.name || firstPartyContractName || - nftCollectionName || - contractDisplayName || + nft?.name || + erc20Token?.name || watchedNftName || + ensName || null; + const image = nft?.image || erc20Token?.image; + const hasPetname = Boolean(nameEntry?.name); return { name, hasPetname, - contractDisplayName, - image: nftCollectionImage, + contractDisplayName: erc20Token?.name, + image, }; }); } -/** - * Attempts to resolve the name for the given parameters. - * - * @param value - The address or contract address to resolve. - * @param type - The type of value, e.g. NameType.ETHEREUM_ADDRESS. - * @param preferContractSymbol - Applies to recognized contracts when no petname is saved: - * If true the contract symbol (e.g. WBTC) will be used instead of the contract name. - * @returns An object with two properties: - * - `name` {string|null} - The display name, if it can be resolved, otherwise null. - * - `hasPetname` {boolean} - True if there is a petname for the given address. - */ export function useDisplayName( - value: string, - type: NameType, - preferContractSymbol: boolean = false, + request: UseDisplayNameRequest, ): UseDisplayNameResponse { - return useDisplayNames([{ preferContractSymbol, type, value }])[0]; + return useDisplayNames([request])[0]; +} + +function useERC20Tokens( + nameRequests: UseDisplayNameRequest[], +): ({ name?: string; image?: string } | undefined)[] { + const erc20TokensByChain = useSelector(selectERC20TokensByChain); + + return nameRequests.map( + ({ preferContractSymbol, type, value, variation }) => { + if (type !== NameType.ETHEREUM_ADDRESS) { + return undefined; + } + + const contractAddress = value.toLowerCase(); + + const { + iconUrl: image, + name: tokenName, + symbol, + } = erc20TokensByChain?.[variation]?.data?.[contractAddress] ?? {}; + + const name = preferContractSymbol && symbol ? symbol : tokenName; + + return { name, image }; + }, + ); +} + +function useWatchedNFTNames( + nameRequests: UseDisplayNameRequest[], +): (string | undefined)[] { + const watchedNftNamesByAddressByChain = useSelector( + getNftContractsByAddressByChain, + ); + + return nameRequests.map(({ type, value, variation }) => { + if (type !== NameType.ETHEREUM_ADDRESS) { + return undefined; + } + + const contractAddress = value.toLowerCase(); + const watchedNftNamesByAddress = watchedNftNamesByAddressByChain[variation]; + + return watchedNftNamesByAddress?.[contractAddress]?.name; + }); +} + +function useNFTs( + nameRequests: UseDisplayNameRequest[], +): ({ name?: string; image?: string } | undefined)[] { + const requests = nameRequests + .filter(({ type }) => type === NameType.ETHEREUM_ADDRESS) + .map(({ value, variation }) => ({ + chainId: variation, + contractAddress: value, + })); + + const nftCollectionsByAddressByChain = useNftCollectionsMetadata(requests); + + return nameRequests.map( + ({ type, value: contractAddress, variation: chainId }) => { + if (type !== NameType.ETHEREUM_ADDRESS) { + return undefined; + } + + const nftCollectionProperties = + nftCollectionsByAddressByChain[chainId]?.[ + contractAddress.toLowerCase() + ]; + + const isSpam = nftCollectionProperties?.isSpam !== false; + + if (!nftCollectionProperties || isSpam) { + return undefined; + } + + const { name, image } = nftCollectionProperties; + + return { name, image }; + }, + ); +} + +function useDomainResolutions(nameRequests: UseDisplayNameRequest[]) { + const domainResolutions = useSelector(getDomainResolutions); + + return nameRequests.map(({ type, value }) => { + if (type !== NameType.ETHEREUM_ADDRESS) { + return undefined; + } + + const matchedResolution = domainResolutions?.find( + (resolution: { + addressBookEntryName: string; + domainName: string; + protocol: string; + resolvedAddress: string; + resolvingSnap: string; + }) => + toChecksumHexAddress(resolution.resolvedAddress) === + toChecksumHexAddress(value), + ); + + const ensName = matchedResolution?.domainName; + + return ensName; + }); +} + +function useFirstPartyContractNames(nameRequests: UseDisplayNameRequest[]) { + return nameRequests.map(({ type, value, variation }) => { + if (type !== NameType.ETHEREUM_ADDRESS) { + return undefined; + } + + const normalizedContractAddress = value.toLowerCase(); + + const contractNames = Object.keys( + FIRST_PARTY_CONTRACT_NAMES, + ) as EXPERIENCES_TYPE[]; + + return contractNames.find((contractName) => { + const currentContractAddress = + FIRST_PARTY_CONTRACT_NAMES[contractName]?.[variation as Hex]; + + return ( + currentContractAddress?.toLowerCase() === normalizedContractAddress + ); + }); + }); } diff --git a/ui/hooks/useFirstPartyContractName.test.ts b/ui/hooks/useFirstPartyContractName.test.ts deleted file mode 100644 index 14d0cd429e6f..000000000000 --- a/ui/hooks/useFirstPartyContractName.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NameType } from '@metamask/name-controller'; -import { getCurrentChainId } from '../selectors'; -import { CHAIN_IDS } from '../../shared/constants/network'; -import { useFirstPartyContractName } from './useFirstPartyContractName'; - -jest.mock('react-redux', () => ({ - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - useSelector: (selector: any) => selector(), -})); - -jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), - getNames: jest.fn(), -})); - -const BRIDGE_NAME_MOCK = 'MetaMask Bridge'; -const BRIDGE_MAINNET_ADDRESS_MOCK = - '0x0439e60F02a8900a951603950d8D4527f400C3f1'; -const BRIDGE_OPTIMISM_ADDRESS_MOCK = - '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e'; -const UNKNOWN_ADDRESS_MOCK = '0xabc123'; - -describe('useFirstPartyContractName', () => { - const getCurrentChainIdMock = jest.mocked(getCurrentChainId); - beforeEach(() => { - jest.resetAllMocks(); - - getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); - }); - - it('returns null if no name found', () => { - const name = useFirstPartyContractName( - UNKNOWN_ADDRESS_MOCK, - NameType.ETHEREUM_ADDRESS, - ); - - expect(name).toBe(null); - }); - - it('returns name if found', () => { - const name = useFirstPartyContractName( - BRIDGE_MAINNET_ADDRESS_MOCK, - NameType.ETHEREUM_ADDRESS, - ); - expect(name).toBe(BRIDGE_NAME_MOCK); - }); - - it('uses variation if specified', () => { - const name = useFirstPartyContractName( - BRIDGE_OPTIMISM_ADDRESS_MOCK, - NameType.ETHEREUM_ADDRESS, - CHAIN_IDS.OPTIMISM, - ); - - expect(name).toBe(BRIDGE_NAME_MOCK); - }); - - it('returns null if type is not address', () => { - const alternateType = 'alternateType' as NameType; - - const name = useFirstPartyContractName( - BRIDGE_MAINNET_ADDRESS_MOCK, - alternateType, - ); - - expect(name).toBe(null); - }); - - it('normalizes addresses to lowercase', () => { - const name = useFirstPartyContractName( - BRIDGE_MAINNET_ADDRESS_MOCK.toUpperCase(), - NameType.ETHEREUM_ADDRESS, - ); - - expect(name).toBe(BRIDGE_NAME_MOCK); - }); -}); diff --git a/ui/hooks/useFirstPartyContractName.ts b/ui/hooks/useFirstPartyContractName.ts deleted file mode 100644 index 47468b472955..000000000000 --- a/ui/hooks/useFirstPartyContractName.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NameType } from '@metamask/name-controller'; -import { useSelector } from 'react-redux'; -import { getCurrentChainId } from '../selectors'; -import { - EXPERIENCES_TYPE, - FIRST_PARTY_CONTRACT_NAMES, -} from '../../shared/constants/first-party-contracts'; - -export type UseFirstPartyContractNameRequest = { - value: string; - type: NameType; - variation?: string; -}; - -export function useFirstPartyContractNames( - requests: UseFirstPartyContractNameRequest[], -): (string | null)[] { - const currentChainId = useSelector(getCurrentChainId); - - return requests.map(({ type, value, variation }) => { - if (type !== NameType.ETHEREUM_ADDRESS) { - return null; - } - - const chainId = variation ?? currentChainId; - const normalizedValue = value.toLowerCase(); - - return ( - Object.keys(FIRST_PARTY_CONTRACT_NAMES).find( - (name) => - FIRST_PARTY_CONTRACT_NAMES[name as EXPERIENCES_TYPE]?.[ - chainId - ]?.toLowerCase() === normalizedValue, - ) ?? null - ); - }); -} - -export function useFirstPartyContractName( - value: string, - type: NameType, - variation?: string, -): string | null { - return useFirstPartyContractNames([{ value, type, variation }])[0]; -} diff --git a/ui/hooks/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js index 5ad37925054b..abbaf0db0bb9 100644 --- a/ui/hooks/useGasFeeEstimates.js +++ b/ui/hooks/useGasFeeEstimates.js @@ -74,9 +74,10 @@ export function useGasFeeEstimates(_networkClientId) { }, [networkClientId]); usePolling({ - startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + startPolling: (input) => + gasFeeStartPollingByNetworkClientId(input.networkClientId), stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - networkClientId, + input: { networkClientId }, }); return { diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js index 0187ac793bbe..dd63e10581d0 100644 --- a/ui/hooks/useGasFeeEstimates.test.js +++ b/ui/hooks/useGasFeeEstimates.test.js @@ -8,7 +8,6 @@ import { getIsNetworkBusyByChainId, } from '../ducks/metamask/metamask'; import { - gasFeeStartPollingByNetworkClientId, gasFeeStopPollingByPollingToken, getNetworkConfigurationByNetworkClientId, } from '../store/actions'; @@ -115,9 +114,9 @@ describe('useGasFeeEstimates', () => { renderHook(() => useGasFeeEstimates()); }); expect(usePolling).toHaveBeenCalledWith({ - startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + startPolling: expect.any(Function), stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - networkClientId: 'selectedNetworkClientId', + input: { networkClientId: 'selectedNetworkClientId' }, }); }); @@ -127,9 +126,9 @@ describe('useGasFeeEstimates', () => { renderHook(() => useGasFeeEstimates('networkClientId1')); }); expect(usePolling).toHaveBeenCalledWith({ - startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + startPolling: expect.any(Function), stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - networkClientId: 'networkClientId1', + input: { networkClientId: 'networkClientId1' }, }); }); diff --git a/ui/hooks/useMMICustodySendTransaction.ts b/ui/hooks/useMMICustodySendTransaction.ts index 0c05d9e16f96..49634fbf0174 100644 --- a/ui/hooks/useMMICustodySendTransaction.ts +++ b/ui/hooks/useMMICustodySendTransaction.ts @@ -34,9 +34,9 @@ export function useMMICustodySendTransaction() { const accountType = useSelector(getAccountType); const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); - const { currentConfirmation } = useConfirmContext() as unknown as { - currentConfirmation: TransactionMeta | undefined; - }; + const { currentConfirmation } = useConfirmContext< + TransactionMeta | undefined + >(); const { from } = getConfirmationSender(currentConfirmation); const fromChecksumHexAddress = toChecksumHexAddress(from || ''); diff --git a/ui/hooks/useMultiPolling.ts b/ui/hooks/useMultiPolling.ts new file mode 100644 index 000000000000..f0b3ed33cdfc --- /dev/null +++ b/ui/hooks/useMultiPolling.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; + +type UseMultiPollingOptions = { + startPolling: (input: PollingInput) => Promise; + stopPollingByPollingToken: (pollingToken: string) => void; + input: PollingInput[]; +}; + +// A hook that manages multiple polling loops of a polling controller. +// Callers provide an array of inputs, and the hook manages starting +// and stopping polling loops for each input. +const useMultiPolling = ( + usePollingOptions: UseMultiPollingOptions, +) => { + const [polls, setPolls] = useState(new Map()); + + useEffect(() => { + // start new polls + for (const input of usePollingOptions.input) { + const key = JSON.stringify(input); + if (!polls.has(key)) { + usePollingOptions + .startPolling(input) + .then((token) => + setPolls((prevPolls) => new Map(prevPolls).set(key, token)), + ); + } + } + + // stop existing polls + for (const [inputKey, token] of polls.entries()) { + const exists = usePollingOptions.input.some( + (i) => inputKey === JSON.stringify(i), + ); + + if (!exists) { + usePollingOptions.stopPollingByPollingToken(token); + setPolls((prevPolls) => { + const newPolls = new Map(prevPolls); + newPolls.delete(inputKey); + return newPolls; + }); + } + } + }, [usePollingOptions.input && JSON.stringify(usePollingOptions.input)]); + + // stop all polling on dismount + useEffect(() => { + return () => { + for (const token of polls.values()) { + usePollingOptions.stopPollingByPollingToken(token); + } + }; + }, []); +}; + +export default useMultiPolling; diff --git a/ui/hooks/useMultichainSelector.ts b/ui/hooks/useMultichainSelector.ts index 326ac79bf9cd..9bd979df7e7e 100644 --- a/ui/hooks/useMultichainSelector.ts +++ b/ui/hooks/useMultichainSelector.ts @@ -11,6 +11,7 @@ export function useMultichainSelector< ) { return useSelector((state: TState) => { // We either pass an account or fallback to the currently selected one + // @ts-expect-error state types don't match return selector(state, account || getSelectedInternalAccount(state)); }); } diff --git a/ui/hooks/useName.test.ts b/ui/hooks/useName.test.ts index 76bd5dc593ad..b102e9dce7a0 100644 --- a/ui/hooks/useName.test.ts +++ b/ui/hooks/useName.test.ts @@ -5,7 +5,7 @@ import { NameOrigin, NameType, } from '@metamask/name-controller'; -import { getCurrentChainId, getNames } from '../selectors'; +import { getNames } from '../selectors'; import { useName } from './useName'; jest.mock('react-redux', () => ({ @@ -15,17 +15,17 @@ jest.mock('react-redux', () => ({ })); jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), getNames: jest.fn(), })); -const CHAIN_ID_MOCK = '0x1'; -const CHAIN_ID_2_MOCK = '0x2'; +const VARIATION_MOCK = '0x1'; +const VARIATION_2_MOCK = '0x2'; const VALUE_MOCK = '0xabc123'; const TYPE_MOCK = NameType.ETHEREUM_ADDRESS; const NAME_MOCK = 'TestName'; const SOURCE_ID_MOCK = 'TestSourceId'; const ORIGIN_MOCK = NameOrigin.API; + const PROPOSED_NAMES_MOCK = { [SOURCE_ID_MOCK]: { proposedNames: ['TestProposedName', 'TestProposedName2'], @@ -35,7 +35,6 @@ const PROPOSED_NAMES_MOCK = { }; describe('useName', () => { - const getCurrentChainIdMock = jest.mocked(getCurrentChainId); const getNamesMock = // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -43,14 +42,12 @@ describe('useName', () => { beforeEach(() => { jest.resetAllMocks(); - - getCurrentChainIdMock.mockReturnValue(CHAIN_ID_MOCK); }); it('returns default values if no state', () => { getNamesMock.mockReturnValue({} as NameControllerState['names']); - const nameEntry = useName(VALUE_MOCK, TYPE_MOCK); + const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK); expect(nameEntry).toStrictEqual({ name: null, @@ -64,7 +61,7 @@ describe('useName', () => { getNamesMock.mockReturnValue({ [TYPE_MOCK]: { [VALUE_MOCK]: { - [CHAIN_ID_2_MOCK]: { + [VARIATION_2_MOCK]: { name: NAME_MOCK, proposedNames: PROPOSED_NAMES_MOCK, sourceId: SOURCE_ID_MOCK, @@ -74,7 +71,7 @@ describe('useName', () => { }, }); - const nameEntry = useName(VALUE_MOCK, TYPE_MOCK); + const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK); expect(nameEntry).toStrictEqual({ name: null, @@ -88,7 +85,7 @@ describe('useName', () => { getNamesMock.mockReturnValue({ [TYPE_MOCK]: { [VALUE_MOCK]: { - [CHAIN_ID_MOCK]: { + [VARIATION_MOCK]: { name: NAME_MOCK, proposedNames: PROPOSED_NAMES_MOCK, sourceId: SOURCE_ID_MOCK, @@ -98,7 +95,7 @@ describe('useName', () => { }, }); - const nameEntry = useName(VALUE_MOCK, TYPE_MOCK); + const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK); expect(nameEntry).toStrictEqual({ name: NAME_MOCK, @@ -112,7 +109,7 @@ describe('useName', () => { getNamesMock.mockReturnValue({ [TYPE_MOCK]: { [VALUE_MOCK]: { - [CHAIN_ID_2_MOCK]: { + [VARIATION_2_MOCK]: { name: NAME_MOCK, proposedNames: PROPOSED_NAMES_MOCK, sourceId: SOURCE_ID_MOCK, @@ -122,7 +119,7 @@ describe('useName', () => { }, }); - const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK); + const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK); expect(nameEntry).toStrictEqual({ name: NAME_MOCK, @@ -147,7 +144,7 @@ describe('useName', () => { }, }); - const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK); + const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK); expect(nameEntry).toStrictEqual({ name: NAME_MOCK, @@ -161,7 +158,7 @@ describe('useName', () => { getNamesMock.mockReturnValue({ [TYPE_MOCK]: { [VALUE_MOCK]: { - [CHAIN_ID_2_MOCK]: { + [VARIATION_2_MOCK]: { name: null, proposedNames: PROPOSED_NAMES_MOCK, sourceId: null, @@ -177,7 +174,7 @@ describe('useName', () => { }, }); - const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK); + const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK); expect(nameEntry).toStrictEqual({ name: NAME_MOCK, @@ -188,37 +185,11 @@ describe('useName', () => { }); }); - it('uses empty string as variation if not specified and type is not address', () => { - const alternateType = 'alternateType' as NameType; - - getNamesMock.mockReturnValue({ - [alternateType]: { - [VALUE_MOCK]: { - '': { - name: NAME_MOCK, - proposedNames: PROPOSED_NAMES_MOCK, - sourceId: SOURCE_ID_MOCK, - origin: ORIGIN_MOCK, - }, - }, - }, - }); - - const nameEntry = useName(VALUE_MOCK, alternateType); - - expect(nameEntry).toStrictEqual({ - name: NAME_MOCK, - sourceId: SOURCE_ID_MOCK, - proposedNames: PROPOSED_NAMES_MOCK, - origin: ORIGIN_MOCK, - }); - }); - it('normalizes addresses to lowercase', () => { getNamesMock.mockReturnValue({ [TYPE_MOCK]: { [VALUE_MOCK]: { - [CHAIN_ID_MOCK]: { + [VARIATION_MOCK]: { name: NAME_MOCK, proposedNames: PROPOSED_NAMES_MOCK, sourceId: SOURCE_ID_MOCK, @@ -228,7 +199,7 @@ describe('useName', () => { }, }); - const nameEntry = useName('0xAbC123', TYPE_MOCK); + const nameEntry = useName('0xAbC123', TYPE_MOCK, VARIATION_MOCK); expect(nameEntry).toStrictEqual({ name: NAME_MOCK, diff --git a/ui/hooks/useName.ts b/ui/hooks/useName.ts index 3af9b0457f79..dd587e81abf6 100644 --- a/ui/hooks/useName.ts +++ b/ui/hooks/useName.ts @@ -5,32 +5,29 @@ import { } from '@metamask/name-controller'; import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; -import { getCurrentChainId, getNames } from '../selectors'; +import { getNames } from '../selectors'; export type UseNameRequest = { value: string; type: NameType; - variation?: string; + variation: string; }; export function useName( value: string, type: NameType, - variation?: string, + variation: string, ): NameEntry { return useNames([{ value, type, variation }])[0]; } export function useNames(requests: UseNameRequest[]): NameEntry[] { const names = useSelector(getNames, isEqual); - const chainId = useSelector(getCurrentChainId); return requests.map(({ value, type, variation }) => { const normalizedValue = normalizeValue(value, type); - const typeVariationKey = getVariationKey(type, chainId); - const variationKey = variation ?? typeVariationKey; const variationsToNameEntries = names[type]?.[normalizedValue] ?? {}; - const variationEntry = variationsToNameEntries[variationKey]; + const variationEntry = variationsToNameEntries[variation]; const fallbackEntry = variationsToNameEntries[FALLBACK_VARIATION]; const entry = @@ -63,13 +60,3 @@ function normalizeValue(value: string, type: string): string { return value; } } - -function getVariationKey(type: string, chainId: string): string { - switch (type) { - case NameType.ETHEREUM_ADDRESS: - return chainId; - - default: - return ''; - } -} diff --git a/ui/hooks/useNftCollectionsMetadata.test.ts b/ui/hooks/useNftCollectionsMetadata.test.ts index 4897e449e6ad..cf7997cb518b 100644 --- a/ui/hooks/useNftCollectionsMetadata.test.ts +++ b/ui/hooks/useNftCollectionsMetadata.test.ts @@ -1,6 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; import { TokenStandard } from '../../shared/constants/transaction'; -import { getCurrentChainId } from '../selectors'; import { getNFTContractInfo, getTokenStandardAndDetails, @@ -17,10 +16,6 @@ jest.mock('react-redux', () => ({ useSelector: (selector: any) => selector(), })); -jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), -})); - jest.mock('../store/actions', () => ({ getNFTContractInfo: jest.fn(), getTokenStandardAndDetails: jest.fn(), @@ -42,7 +37,6 @@ const ERC_721_COLLECTION_2_MOCK = { }; describe('useNftCollectionsMetadata', () => { - const mockGetCurrentChainId = jest.mocked(getCurrentChainId); const mockGetNFTContractInfo = jest.mocked(getNFTContractInfo); const mockGetTokenStandardAndDetails = jest.mocked( getTokenStandardAndDetails, @@ -50,7 +44,6 @@ describe('useNftCollectionsMetadata', () => { beforeEach(() => { jest.resetAllMocks(); - mockGetCurrentChainId.mockReturnValue(CHAIN_ID_MOCK); mockGetNFTContractInfo.mockResolvedValue({ collections: [ERC_721_COLLECTION_1_MOCK, ERC_721_COLLECTION_2_MOCK], }); @@ -67,10 +60,12 @@ describe('useNftCollectionsMetadata', () => { const { result, waitForNextUpdate } = renderHook(() => useNftCollectionsMetadata([ { - value: ERC_721_ADDRESS_1, + chainId: CHAIN_ID_MOCK, + contractAddress: ERC_721_ADDRESS_1, }, { - value: ERC_721_ADDRESS_2, + chainId: CHAIN_ID_MOCK, + contractAddress: ERC_721_ADDRESS_2, }, ]), ); @@ -79,8 +74,10 @@ describe('useNftCollectionsMetadata', () => { expect(mockGetNFTContractInfo).toHaveBeenCalledTimes(1); expect(result.current).toStrictEqual({ - [ERC_721_ADDRESS_1.toLowerCase()]: ERC_721_COLLECTION_1_MOCK, - [ERC_721_ADDRESS_2.toLowerCase()]: ERC_721_COLLECTION_2_MOCK, + [CHAIN_ID_MOCK]: { + [ERC_721_ADDRESS_1.toLowerCase()]: ERC_721_COLLECTION_1_MOCK, + [ERC_721_ADDRESS_2.toLowerCase()]: ERC_721_COLLECTION_2_MOCK, + }, }); }); @@ -99,7 +96,8 @@ describe('useNftCollectionsMetadata', () => { renderHook(() => useNftCollectionsMetadata([ { - value: '0xERC20Address', + chainId: CHAIN_ID_MOCK, + contractAddress: '0xERC20Address', }, ]), ); @@ -114,7 +112,8 @@ describe('useNftCollectionsMetadata', () => { renderHook(() => useNftCollectionsMetadata([ { - value: '0xERC20Address', + chainId: CHAIN_ID_MOCK, + contractAddress: '0xERC20Address', }, ]), ); @@ -126,10 +125,12 @@ describe('useNftCollectionsMetadata', () => { const { waitForNextUpdate, rerender } = renderHook(() => useNftCollectionsMetadata([ { - value: ERC_721_ADDRESS_1, + chainId: CHAIN_ID_MOCK, + contractAddress: ERC_721_ADDRESS_1, }, { - value: ERC_721_ADDRESS_2, + chainId: CHAIN_ID_MOCK, + contractAddress: ERC_721_ADDRESS_2, }, ]), ); diff --git a/ui/hooks/useNftCollectionsMetadata.ts b/ui/hooks/useNftCollectionsMetadata.ts index 641e0fb25dcd..e71216e254c9 100644 --- a/ui/hooks/useNftCollectionsMetadata.ts +++ b/ui/hooks/useNftCollectionsMetadata.ts @@ -1,9 +1,5 @@ -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { Collection } from '@metamask/assets-controllers'; -import type { Hex } from '@metamask/utils'; import { TokenStandard } from '../../shared/constants/transaction'; -import { getCurrentChainId } from '../selectors'; import { getNFTContractInfo, getTokenStandardAndDetails, @@ -11,28 +7,62 @@ import { import { useAsyncResult } from './useAsyncResult'; export type UseNftCollectionsMetadataRequest = { - value: string; - chainId?: string; -}; - -type CollectionsData = { - [key: string]: Collection; + chainId: string; + contractAddress: string; }; // For now, we only support ERC721 tokens const SUPPORTED_NFT_TOKEN_STANDARDS = [TokenStandard.ERC721]; -async function fetchCollections( - memoisedContracts: string[], +export function useNftCollectionsMetadata( + requests: UseNftCollectionsMetadataRequest[], +): Record> { + const { value: collectionsMetadata } = useAsyncResult( + () => fetchCollections(requests), + [JSON.stringify(requests)], + ); + + return collectionsMetadata ?? {}; +} + +async function fetchCollections(requests: UseNftCollectionsMetadataRequest[]) { + const valuesByChainId = requests.reduce>( + (acc, { chainId, contractAddress }) => { + acc[chainId] = [...(acc[chainId] ?? []), contractAddress.toLowerCase()]; + return acc; + }, + {}, + ); + + const chainIds = Object.keys(valuesByChainId); + + const responses = await Promise.all( + chainIds.map((chainId) => { + const contractAddresses = valuesByChainId[chainId]; + return fetchCollectionsForChain(contractAddresses, chainId); + }), + ); + + return chainIds.reduce>>( + (acc, chainId, index) => { + acc[chainId] = responses[index]; + return acc; + }, + {}, + ); +} + +async function fetchCollectionsForChain( + contractAddresses: string[], chainId: string, -): Promise { +) { const contractStandardsResponses = await Promise.all( - memoisedContracts.map((contractAddress) => + contractAddresses.map((contractAddress) => getTokenStandardAndDetails(contractAddress, chainId), ), ); - const supportedNFTContracts = memoisedContracts.filter( + const supportedNFTContracts = contractAddresses.filter( (_contractAddress, index) => SUPPORTED_NFT_TOKEN_STANDARDS.includes( contractStandardsResponses[index].standard as TokenStandard, @@ -48,37 +78,16 @@ async function fetchCollections( chainId, ); - const collectionsData: CollectionsData = collectionsResult.collections.reduce( - (acc: CollectionsData, collection, index) => { - acc[supportedNFTContracts[index]] = { - name: collection?.name, - image: collection?.image, - isSpam: collection?.isSpam, - }; - return acc; - }, - {}, - ); + const collectionsData = collectionsResult.collections.reduce< + Record + >((acc, collection, index) => { + acc[supportedNFTContracts[index]] = { + name: collection?.name, + image: collection?.image, + isSpam: collection?.isSpam, + }; + return acc; + }, {}); return collectionsData; } - -export function useNftCollectionsMetadata( - requests: UseNftCollectionsMetadataRequest[], - providedChainId?: Hex, -) { - const chainId = useSelector(getCurrentChainId) || providedChainId; - - const memoisedContracts = useMemo(() => { - return requests - .filter(({ value }) => value) - .map(({ value }) => value.toLowerCase()); - }, [requests]); - - const { value: collectionsMetadata } = useAsyncResult( - () => fetchCollections(memoisedContracts, chainId), - [JSON.stringify(memoisedContracts), chainId], - ); - - return collectionsMetadata || {}; -} diff --git a/ui/hooks/usePolling.test.js b/ui/hooks/usePolling.test.js index 9250257d3cbc..a556bb86be54 100644 --- a/ui/hooks/usePolling.test.js +++ b/ui/hooks/usePolling.test.js @@ -4,13 +4,12 @@ import usePolling from './usePolling'; describe('usePolling', () => { // eslint-disable-next-line jest/no-done-callback - it('calls startPollingByNetworkClientId and callback option args with polling token when component instantiating the hook mounts', (done) => { + it('calls startPolling and calls back with polling token when component instantiating the hook mounts', (done) => { const mockStart = jest.fn().mockImplementation(() => { return Promise.resolve('pollingToken'); }); const mockStop = jest.fn(); const networkClientId = 'mainnet'; - const options = {}; const mockState = { metamask: {}, }; @@ -18,17 +17,16 @@ describe('usePolling', () => { renderHookWithProvider(() => { usePolling({ callback: (pollingToken) => { - expect(mockStart).toHaveBeenCalledWith(networkClientId, options); + expect(mockStart).toHaveBeenCalledWith({ networkClientId }); expect(pollingToken).toBeDefined(); done(); return (_pollingToken) => { // noop }; }, - startPollingByNetworkClientId: mockStart, + startPolling: mockStart, stopPollingByPollingToken: mockStop, - networkClientId, - options, + input: { networkClientId }, }); }, mockState); }); @@ -39,7 +37,6 @@ describe('usePolling', () => { }); const mockStop = jest.fn(); const networkClientId = 'mainnet'; - const options = {}; const mockState = { metamask: {}, }; @@ -54,10 +51,9 @@ describe('usePolling', () => { done(); }; }, - startPollingByNetworkClientId: mockStart, + startPolling: mockStart, stopPollingByPollingToken: mockStop, - networkClientId, - options, + input: { networkClientId }, }), mockState, ); diff --git a/ui/hooks/usePolling.ts b/ui/hooks/usePolling.ts index 1a9d6b1f576e..613e70cf17b5 100644 --- a/ui/hooks/usePolling.ts +++ b/ui/hooks/usePolling.ts @@ -1,22 +1,16 @@ import { useEffect, useRef } from 'react'; -type UsePollingOptions = { +type UsePollingOptions = { callback?: (pollingToken: string) => (pollingToken: string) => void; - startPollingByNetworkClientId: ( - networkClientId: string, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options: any, - ) => Promise; + startPolling: (input: PollingInput) => Promise; stopPollingByPollingToken: (pollingToken: string) => void; - networkClientId: string; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options?: any; + input: PollingInput; enabled?: boolean; }; -const usePolling = (usePollingOptions: UsePollingOptions) => { +const usePolling = ( + usePollingOptions: UsePollingOptions, +) => { const pollTokenRef = useRef(null); const cleanupRef = useRef void)>(null); let isMounted = false; @@ -38,10 +32,7 @@ const usePolling = (usePollingOptions: UsePollingOptions) => { // Start polling when the component mounts usePollingOptions - .startPollingByNetworkClientId( - usePollingOptions.networkClientId, - usePollingOptions.options, - ) + .startPolling(usePollingOptions.input) .then((pollToken) => { pollTokenRef.current = pollToken; cleanupRef.current = usePollingOptions.callback?.(pollToken) || null; @@ -56,12 +47,7 @@ const usePolling = (usePollingOptions: UsePollingOptions) => { cleanup(); }; }, [ - usePollingOptions.networkClientId, - usePollingOptions.options && - JSON.stringify( - usePollingOptions.options, - Object.keys(usePollingOptions.options).sort(), - ), + usePollingOptions.input && JSON.stringify(usePollingOptions.input), usePollingOptions.enabled, ]); }; diff --git a/ui/hooks/useTokenRatesPolling.ts b/ui/hooks/useTokenRatesPolling.ts new file mode 100644 index 000000000000..41c1c8793b97 --- /dev/null +++ b/ui/hooks/useTokenRatesPolling.ts @@ -0,0 +1,40 @@ +import { useSelector } from 'react-redux'; +import { + getMarketData, + getNetworkConfigurationsByChainId, + getTokenExchangeRates, + getTokensMarketData, + getUseCurrencyRateCheck, +} from '../selectors'; +import { + tokenRatesStartPolling, + tokenRatesStopPollingByPollingToken, +} from '../store/actions'; +import useMultiPolling from './useMultiPolling'; + +const useTokenRatesPolling = ({ chainIds }: { chainIds?: string[] } = {}) => { + // Selectors to determine polling input + const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + + // Selectors returning state updated by the polling + const tokenExchangeRates = useSelector(getTokenExchangeRates); + const tokensMarketData = useSelector(getTokensMarketData); + const marketData = useSelector(getMarketData); + + useMultiPolling({ + startPolling: tokenRatesStartPolling, + stopPollingByPollingToken: tokenRatesStopPollingByPollingToken, + input: useCurrencyRateCheck + ? chainIds ?? Object.keys(networkConfigurations) + : [], + }); + + return { + tokenExchangeRates, + tokensMarketData, + marketData, + }; +}; + +export default useTokenRatesPolling; diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index 95828e3e250e..b5ebc0a83eb6 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -268,17 +268,17 @@ exports[`AssetPage should render a native asset 1`] = `

- Start your journey with ETH + Tips for using a wallet

- Get started with web3 by adding some ETH to your wallet. + Adding tokens unlocks more ways to use web3.

- Start your journey with ETH + Tips for using a wallet

- Get started with web3 by adding some ETH to your wallet. + Adding tokens unlocks more ways to use web3.

$1.00

@@ -777,7 +777,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` style="padding-right: 100%; direction: rtl;" >

$1.00

@@ -1136,17 +1136,17 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `

- Start your journey with ETH + Tips for using a wallet

- Get started with web3 by adding some ETH to your wallet. + Adding tokens unlocks more ways to use web3.

- Select token + Select token and amount
diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts index 07c35ae57749..d8cba6c109b0 100644 --- a/ui/pages/bridge/bridge.util.test.ts +++ b/ui/pages/bridge/bridge.util.test.ts @@ -1,13 +1,28 @@ import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { CHAIN_IDS } from '../../../shared/constants/network'; -import { fetchBridgeFeatureFlags, fetchBridgeTokens } from './bridge.util'; +import mockBridgeQuotesErc20Erc20 from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; +import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json'; +import { zeroAddress } from '../../__mocks__/ethereumjs-util'; +import { + fetchBridgeFeatureFlags, + fetchBridgeQuotes, + fetchBridgeTokens, +} from './bridge.util'; jest.mock('../../../shared/lib/fetch-with-cache'); describe('Bridge utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('fetchBridgeFeatureFlags', () => { it('should fetch bridge feature flags successfully', async () => { const mockResponse = { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + }, 'extension-support': true, 'src-network-allowlist': [1, 10, 59144, 120], 'dest-network-allowlist': [1, 137, 59144, 11111], @@ -28,6 +43,10 @@ describe('Bridge utils', () => { }); expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 1, + refreshRate: 3, + }, extensionSupport: true, srcNetworkAllowlist: [ CHAIN_IDS.MAINNET, @@ -46,8 +65,10 @@ describe('Bridge utils', () => { it('should use fallback bridge feature flags if response is unexpected', async () => { const mockResponse = { - flag1: true, - flag2: false, + 'extension-support': 25, + 'src-network-allowlist': ['a', 'b', 1], + a: 'b', + 'dest-network-allowlist': [1, 137, 59144, 11111], }; (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); @@ -65,6 +86,10 @@ describe('Bridge utils', () => { }); expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 5, + refreshRate: 30000, + }, extensionSupport: false, srcNetworkAllowlist: [], destNetworkAllowlist: [], @@ -141,4 +166,113 @@ describe('Bridge utils', () => { await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError); }); }); + + describe('fetchBridgeQuotes', () => { + it('should fetch bridge quotes successfully, no approvals', async () => { + (fetchWithCache as jest.Mock).mockResolvedValue( + mockBridgeQuotesNativeErc20, + ); + + const result = await fetchBridgeQuotes({ + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: zeroAddress(), + destTokenAddress: zeroAddress(), + srcTokenAmount: '20000', + slippage: 0.5, + }); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 0 }, + functionName: 'fetchBridgeQuotes', + }); + + expect(result).toStrictEqual(mockBridgeQuotesNativeErc20); + }); + + it('should fetch bridge quotes successfully, with approvals', async () => { + (fetchWithCache as jest.Mock).mockResolvedValue([ + ...mockBridgeQuotesErc20Erc20, + { ...mockBridgeQuotesErc20Erc20[0], approval: null }, + { ...mockBridgeQuotesErc20Erc20[0], trade: null }, + ]); + + const result = await fetchBridgeQuotes({ + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: zeroAddress(), + destTokenAddress: zeroAddress(), + srcTokenAmount: '20000', + slippage: 0.5, + }); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 0 }, + functionName: 'fetchBridgeQuotes', + }); + + expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); + }); + + it('should filter out malformed bridge quotes', async () => { + (fetchWithCache as jest.Mock).mockResolvedValue([ + ...mockBridgeQuotesErc20Erc20, + ...mockBridgeQuotesErc20Erc20.map( + ({ quote, ...restOfQuote }) => restOfQuote, + ), + { + ...mockBridgeQuotesErc20Erc20[0], + quote: { + srcAsset: { + ...mockBridgeQuotesErc20Erc20[0].quote.srcAsset, + decimals: undefined, + }, + }, + }, + { + ...mockBridgeQuotesErc20Erc20[1], + quote: { + srcAsset: { + ...mockBridgeQuotesErc20Erc20[1].quote.destAsset, + address: undefined, + }, + }, + }, + ]); + + const result = await fetchBridgeQuotes({ + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: zeroAddress(), + destTokenAddress: zeroAddress(), + srcTokenAmount: '20000', + slippage: 0.5, + }); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 0 }, + functionName: 'fetchBridgeQuotes', + }); + + expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); + }); + }); }); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index 915a933e7c02..f154b7e62b19 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -11,7 +11,6 @@ import { } from '../../../shared/constants/bridge'; import { MINUTE } from '../../../shared/constants/time'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; -import { validateData } from '../../../shared/lib/swaps-utils'; import { decimalToHex, hexToDecimal, @@ -20,43 +19,37 @@ 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'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; +import { + BridgeAsset, + BridgeFlag, + FeatureFlagResponse, + FeeData, + FeeType, + Quote, + QuoteRequest, + QuoteResponse, + TxData, +} from './types'; +import { + FEATURE_FLAG_VALIDATORS, + QUOTE_VALIDATORS, + TX_DATA_VALIDATORS, + TOKEN_VALIDATORS, + validateResponse, + QUOTE_RESPONSE_VALIDATORS, + FEE_DATA_VALIDATORS, +} from './utils/validators'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; -// Types copied from Metabridge API -enum BridgeFlag { - EXTENSION_SUPPORT = 'extension-support', - NETWORK_SRC_ALLOWLIST = 'src-network-allowlist', - NETWORK_DEST_ALLOWLIST = 'dest-network-allowlist', -} - -export type FeatureFlagResponse = { - [BridgeFlag.EXTENSION_SUPPORT]: boolean; - [BridgeFlag.NETWORK_SRC_ALLOWLIST]: number[]; - [BridgeFlag.NETWORK_DEST_ALLOWLIST]: number[]; -}; -// End of copied types - -type Validator = { - property: keyof ExpectedResponse | string; - type: string; - validator: (value: DataToValidate) => boolean; -}; - -const validateResponse = ( - validators: Validator[], - data: unknown, - urlUsed: string, -): data is ExpectedResponse => { - return validateData(validators, data, urlUsed); -}; - export async function fetchBridgeFeatureFlags(): Promise { const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; const rawFeatureFlags = await fetchWithCache({ @@ -67,35 +60,15 @@ export async function fetchBridgeFeatureFlags(): Promise { }); if ( - validateResponse( - [ - { - property: BridgeFlag.EXTENSION_SUPPORT, - type: 'boolean', - validator: (v) => typeof v === 'boolean', - }, - { - property: BridgeFlag.NETWORK_SRC_ALLOWLIST, - type: 'object', - validator: (v): v is number[] => - Object.values(v as { [s: string]: unknown }).every( - (i) => typeof i === 'number', - ), - }, - { - property: BridgeFlag.NETWORK_DEST_ALLOWLIST, - type: 'object', - validator: (v): v is number[] => - Object.values(v as { [s: string]: unknown }).every( - (i) => typeof i === 'number', - ), - }, - ], + validateResponse( + FEATURE_FLAG_VALIDATORS, rawFeatureFlags, url, ) ) { return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: + rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG], [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: rawFeatureFlags[BridgeFlag.EXTENSION_SUPPORT], [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: rawFeatureFlags[ @@ -108,6 +81,10 @@ export async function fetchBridgeFeatureFlags(): Promise { } return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: 5, + }, // TODO set default to true once bridging is live [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, // TODO set default to ALLOWED_BRIDGE_CHAIN_IDS once bridging is live @@ -142,13 +119,9 @@ export async function fetchBridgeTokens( transformedTokens[nativeToken.address] = nativeToken; } - tokens.forEach((token: SwapsTokenObject) => { + tokens.forEach((token: unknown) => { if ( - validateResponse( - TOKEN_VALIDATORS, - token, - url, - ) && + validateResponse(TOKEN_VALIDATORS, token, url) && !( isSwapsDefaultTokenSymbol(token.symbol, chainId) || isSwapsDefaultTokenAddress(token.address, chainId) @@ -159,3 +132,51 @@ export async function fetchBridgeTokens( }); return transformedTokens; } + +// Returns a list of bridge tx quotes +export async function fetchBridgeQuotes( + request: QuoteRequest, +): Promise { + const queryParams = new URLSearchParams({ + walletAddress: request.walletAddress, + srcChainId: request.srcChainId.toString(), + destChainId: request.destChainId.toString(), + srcTokenAddress: request.srcTokenAddress, + destTokenAddress: request.destTokenAddress, + srcTokenAmount: request.srcTokenAmount, + slippage: request.slippage.toString(), + insufficientBal: request.insufficientBal ? 'true' : 'false', + resetApproval: request.resetApproval ? 'true' : 'false', + }); + const url = `${BRIDGE_API_BASE_URL}/getQuote?${queryParams}`; + const quotes = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: 0 }, + functionName: 'fetchBridgeQuotes', + }); + + const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => { + const { quote, approval, trade } = quoteResponse; + return ( + validateResponse( + QUOTE_RESPONSE_VALIDATORS, + quoteResponse, + url, + ) && + validateResponse(QUOTE_VALIDATORS, quote, url) && + validateResponse(TOKEN_VALIDATORS, quote.srcAsset, url) && + validateResponse(TOKEN_VALIDATORS, quote.destAsset, url) && + validateResponse(TX_DATA_VALIDATORS, trade, url) && + validateResponse( + FEE_DATA_VALIDATORS, + quote.feeData[FeeType.METABRIDGE], + url, + ) && + (approval + ? validateResponse(TX_DATA_VALIDATORS, approval, url) + : true) + ); + }); + return filteredQuotes; +} diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss index 98a3a3ee5c34..bc96dfbbe825 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -1,6 +1,8 @@ @use "design-system"; @import 'prepare/index'; +@import 'quotes/index'; + .bridge { max-height: 100vh; 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 index b406cafe0941..26e25b8bd4cd 100644 --- a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap +++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap @@ -9,7 +9,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] = class="mm-box prepare-bridge-page__content" >
$0.00 @@ -129,7 +130,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] =
$0.00 @@ -211,7 +213,7 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` class="mm-box prepare-bridge-page__content" >
$0.00 @@ -337,7 +340,7 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = `
$0.00 diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx index 5e42823c885b..91adb422e22b 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -3,6 +3,10 @@ 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 mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { RequestStatus } from '../../../../app/scripts/controllers/bridge/constants'; import { BridgeCTAButton } from './bridge-cta-button'; describe('BridgeCTAButton', () => { @@ -25,6 +29,52 @@ describe('BridgeCTAButton', () => { expect(getByRole('button')).toBeDisabled(); }); + it('should render the component when amount is missing', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: null, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Enter amount')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should render the component when amount and dest token is missing', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: null, + fromToken: 'ETH', + toToken: null, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Select token and amount')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + it('should render the component when tx is submittable', () => { const mockStore = createBridgeMockStore( { @@ -37,14 +87,72 @@ describe('BridgeCTAButton', () => { toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - {}, + { + quotes: mockBridgeQuotesNativeErc20, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.FETCHED, + }, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Confirm')).toBeInTheDocument(); + expect(getByRole('button')).not.toBeDisabled(); + }); + + it('should disable the component when quotes are loading and there are no existing quotes', () => { + 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, + }, + { + quotes: [], + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.LOADING, + }, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Fetching quotes...')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should enable the component when quotes are loading and there are existing quotes', () => { + 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, + }, + { + quotes: mockBridgeQuotesNativeErc20, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.LOADING, + }, ); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), ); - expect(getByText('Bridge')).toBeInTheDocument(); + expect(getByText('Confirm')).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 index fedcf4d4606a..28a1a2c1fbd6 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Button } from '../../../components/component-library'; import { + getBridgeQuotes, getFromAmount, getFromChain, getFromToken, @@ -22,16 +23,29 @@ export const BridgeCTAButton = () => { const fromAmount = useSelector(getFromAmount); const toAmount = useSelector(getToAmount); + const { isLoading } = useSelector(getBridgeQuotes); + const isTxSubmittable = fromToken && toToken && fromChain && toChain && fromAmount && toAmount; const label = useMemo(() => { + if (isLoading && !isTxSubmittable) { + return t('swapFetchingQuotes'); + } + + if (!fromAmount) { + if (!toToken) { + return t('bridgeSelectTokenAndAmount'); + } + return t('bridgeEnterAmount'); + } + if (isTxSubmittable) { - return t('bridge'); + return t('confirm'); } return t('swapSelectToken'); - }, [isTxSubmittable]); + }, [isLoading, fromAmount, toToken, isTxSubmittable]); return ( + + +
+
+
+`; + +exports[`BridgeQuoteCard should render the recommended quote while loading new quotes 1`] = ` +
+
+
+
+
+
+

+ Estimated time +

+
+
+ +
+
+
+
+
+

+

+

+ 1 minutes +

+
+
+
+
+

+ Quote rate +

+
+
+
+

+

+

+ 1 ETH = 2465.4630 USDC +

+
+
+
+
+

+ Total fees +

+
+
+ +
+
+
+
+
+

+ 0.01 ETH +

+
+

+ $0.01 +

+
+
+
+ +
+
+`; diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap new file mode 100644 index 000000000000..41d8a03d1ac1 --- /dev/null +++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeQuotesModal should render the modal 1`] = ` + +
+
+
+ -
-
-
-
- - - - -
@@ -189,7 +112,7 @@ exports[`ConfirmDecryptMessage Component should match snapshot when preference i
- 966.987986 ABC + 966.987986 ETH
@@ -210,35 +133,40 @@ exports[`ConfirmDecryptMessage Component should match snapshot when preference i
- {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}} - -
-
-
+ {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"}} + +
+
+
-
- Decrypt message + +
+ Decrypt message +
+
-
+
+
+ Balance: +
+
+ 966.987986 ETH +
+
+
+
+
+ + T + +
+ test would like to read this message to complete your action +
+
+
+
+
+
+ + This message cannot be decrypted due to error: Decrypt inline error +
+
+
+
+ +
+ Decrypt message +
+
+
+
+
+
+
+ +
+
+`; + +exports[`ConfirmDecryptMessage Component shows the correct message data 1`] = ` +
+
+
+
+
+ Decrypt request +
+
+
+
+
+
+
@@ -472,35 +515,66 @@ exports[`ConfirmDecryptMessage Component should match snapshot when preference i
- {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}} - +
+ raw message + +
+
+
+
+ +
+ Decrypt message +
+
+
-
-
- Decrypt message +
+ Copy encrypted message +
+
-