diff --git a/.circleci/config.yml b/.circleci/config.yml index 74815818582f..3836d5e4048e 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: @@ -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/.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/.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/.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/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-41.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-41.0.0-57b3d695bb.patch diff --git a/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch b/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch new file mode 100644 index 000000000000..1c0aa8a99b3a --- /dev/null +++ b/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch @@ -0,0 +1,26 @@ +diff --git a/dist/json-rpc.cjs b/dist/json-rpc.cjs +index 6061f7b8b42f0521b0718d616e5a12a1a7520068..11d0233a7bd4b610a99da6a3d105840e88e108e6 100644 +--- a/dist/json-rpc.cjs ++++ b/dist/json-rpc.cjs +@@ -68,7 +68,7 @@ function createOriginRegExp(matcher) { + const escaped = matcher.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); + // Support wildcards + const regex = escaped.replace(/\\\*/gu, '.*'); +- return RegExp(`${regex}$`, 'u'); ++ return RegExp(`^${regex}$`, 'u'); + } + /** + * Check whether an origin is allowed or not using a matcher string. +diff --git a/dist/json-rpc.mjs b/dist/json-rpc.mjs +index bfa1c2dbbed46a2221ef708afdb97b15db84bc1b..81bc2150cf5d6a9bdabe8d43b04352b299bc1c4d 100644 +--- a/dist/json-rpc.mjs ++++ b/dist/json-rpc.mjs +@@ -63,7 +63,7 @@ function createOriginRegExp(matcher) { + const escaped = matcher.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); + // Support wildcards + const regex = escaped.replace(/\\\*/gu, '.*'); +- return RegExp(`${regex}$`, 'u'); ++ return RegExp(`^${regex}$`, 'u'); + } + /** + * Check whether an origin is allowed or not using a matcher string. diff --git a/CHANGELOG.md b/CHANGELOG.md index 557e5f290d01..e849728f7781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.7.0] +### Added +- Added Token Network Filter UI, allowing users to filter tokens by network (behind a feature flag) ([#27884](https://github.com/MetaMask/metamask-extension/pull/27884)) +- Added Ape token icon for mainnet ([#27974](https://github.com/MetaMask/metamask-extension/pull/27974)) +- Implemented redesigned native asset transfer for both wallet-initiated and dApp-initiated confirmations ([#27979](https://github.com/MetaMask/metamask-extension/pull/27979)) +- Enabled the Security Alerts API with a fallback mechanism to ensure user experience is not disrupted ([#28040](https://github.com/MetaMask/metamask-extension/pull/28040)) +- Added re-simulation logic to the transaction controller ([#28104](https://github.com/MetaMask/metamask-extension/pull/28104)) +- Made the message section in the signature page collapsible and added a copy option ([#28038](https://github.com/MetaMask/metamask-extension/pull/28038)) +- Added token transfer confirmation for ERC721 and ERC1155 tokens ([#27955](https://github.com/MetaMask/metamask-extension/pull/27955)) +- Added support for external links in feature announcements ([#26491](https://github.com/MetaMask/metamask-extension/pull/26491)) +- Added a Notifications option to the settings page ([#26843](https://github.com/MetaMask/metamask-extension/pull/26843)) +- Enabled the use of a preview token to view unpublished content from Contentful ([#27809](https://github.com/MetaMask/metamask-extension/pull/27809)) +- Added account syncing to MetaMask, allowing users to synchronize accounts and account names across devices ([#28120](https://github.com/MetaMask/metamask-extension/pull/28120)) +- Introduced a new phishing warning UI with improved design ([#27942](https://github.com/MetaMask/metamask-extension/pull/27942)) +- Added a privacy mode toggle to hide and show sensitive information and token balances ([#28021](https://github.com/MetaMask/metamask-extension/pull/28021)) +- Added test network to the default selected networks list if it is the globally selected network during a connection request ([#27980](https://github.com/MetaMask/metamask-extension/pull/27980)) + +### Changed +- Allowed users to remove Linea from the networks list and added it to the Popular Networks section ([#27512](https://github.com/MetaMask/metamask-extension/pull/27512)) +- Updated transaction controller to reduce gas limit fallback and remove global network usage from transaction simulation ([#27954](https://github.com/MetaMask/metamask-extension/pull/27954)) +- Reduced usage of scientific notation by implementing a decimals rounding strategy and added tooltips for full values ([#27992](https://github.com/MetaMask/metamask-extension/pull/27992)) +- Improved visibility of decrypted messages and added a "scroll to bottom" button ([#27622](https://github.com/MetaMask/metamask-extension/pull/27622)) +- Updated network message to show the full network name on the Review Permission and Connections pages ([#28126](https://github.com/MetaMask/metamask-extension/pull/28126)) +- Removed the feature flag for the confirmations screen ([#27877](https://github.com/MetaMask/metamask-extension/pull/27877)) + +### Fixed +- Fixed issue where token balance showed as 0 during send flow when navigating from the token details page ([#28136](https://github.com/MetaMask/metamask-extension/pull/28136)) +- Fixed issue where small spending caps were coerced to zero on the approve screen ([#28179](https://github.com/MetaMask/metamask-extension/pull/28179)) +- Fixed gas calculations for low Max base fee and Priority fee ([#28037](https://github.com/MetaMask/metamask-extension/pull/28037)) +- Disabled notifications when Basic functionality is turned off ([#28045]) +- Fixed alignment issues of custom UI links in Snaps ([#27957](https://github.com/MetaMask/metamask-extension/pull/27957)) +- Fixed misalignment of the quote rate in swaps ([#28016](https://github.com/MetaMask/metamask-extension/pull/28016)) +- Prevented scrolling to the account list item on the send page to keep the relevant UI in view ([#27934](https://github.com/MetaMask/metamask-extension/pull/27934)) +- Improved handling of network switching and adding networks to prevent issues with queued transactions ([#28090](https://github.com/MetaMask/metamask-extension/pull/28090)) +- Prevented redirect after adding a network in Onboarding Settings ([#28165](https://github.com/MetaMask/metamask-extension/pull/28165)) + ## [12.6.1] ### Fixed - Fixed gas limit estimation on Base and BNB chains ([#28327](https://github.com/MetaMask/metamask-extension/pull/28327)) @@ -5307,7 +5343,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.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.7.0...HEAD +[12.7.0]: https://github.com/MetaMask/metamask-extension/compare/v12.6.1...v12.7.0 [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 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index fe0c84afcfac..9af24022bcb3 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..308099b1c2b1 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" }, @@ -1900,13 +1879,6 @@ "functionType": { "message": "Τύπος λειτουργίας" }, - "fundYourWallet": { - "message": "Χρηματοδοτήστε το πορτοφόλι σας" - }, - "fundYourWalletDescription": { - "message": "Ξεκινήστε προσθέτοντας περίπου $1 στο πορτοφόλι σας.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Τέλος συναλλαγής" }, @@ -1990,14 +1962,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": "Πηγαίνετε πίσω" }, @@ -4858,9 +4822,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 +4975,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 +5960,6 @@ "viewActivity": { "message": "Προβολή δραστηριότητας" }, - "viewAllDetails": { - "message": "Προβολή όλων των λεπτομερειών" - }, "viewAllQuotes": { "message": "προβολή όλων των προσφορών" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1c45a0cd6a6e..512eb3b0ee36 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" @@ -888,12 +875,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 +882,6 @@ "buyNow": { "message": "Buy Now" }, - "buyToken": { - "message": "Buy $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1190,10 +1167,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 +1349,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 +1512,9 @@ "dcent": { "message": "D'Cent" }, + "debitCreditPurchaseOptions": { + "message": "Debit or credit card purchase options" + }, "decimal": { "message": "Token decimal" }, @@ -2119,12 +2107,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 +2199,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" }, @@ -4487,6 +4457,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 +4648,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 +4743,6 @@ "selectEnableDisplayMediaPrivacyPreference": { "message": "Turn on Display NFT Media" }, - "selectFundingMethod": { - "message": "Select a funding method" - }, "selectHdPath": { "message": "Select HD path" }, @@ -5244,9 +5218,6 @@ "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." - }, "somethingDoesntLookRight": { "message": "Something doesn't look right? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." @@ -5419,14 +5390,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." }, @@ -6030,6 +5993,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 +6051,9 @@ "tokenList": { "message": "Token lists" }, + "tokenMarketplace": { + "message": "Token marketplace" + }, "tokenScamSecurityRisk": { "message": "token scams and security risks" }, @@ -6330,9 +6302,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 +6430,6 @@ "viewActivity": { "message": "View activity" }, - "viewAllDetails": { - "message": "View all details" - }, "viewAllQuotes": { "message": "view all quotes" }, @@ -6546,6 +6512,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/es/messages.json b/app/_locales/es/messages.json index 97d6f4be9854..ada162b9a12b 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" }, @@ -1897,13 +1876,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 +1959,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" }, @@ -4855,9 +4819,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 +4972,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 +5957,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..cebfc3cef106 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" }, @@ -2335,9 +2318,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..856638ba2b8a 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..45e64a972e17 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": "बाइट" }, @@ -1900,13 +1879,6 @@ "functionType": { "message": "फ़ंक्शन का प्रकार" }, - "fundYourWallet": { - "message": "अपने वॉलेट को फंड करें" - }, - "fundYourWalletDescription": { - "message": "अपने वॉलेट में कुछ $1 जोड़कर शुरुआत करें।", - "description": "$1 is the token symbol" - }, "gas": { "message": "गैस" }, @@ -1990,14 +1962,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": "वापस जाएं" }, @@ -4858,9 +4822,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 +4975,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 +5960,6 @@ "viewActivity": { "message": "एक्टिविटी देखें" }, - "viewAllDetails": { - "message": "सभी विवरण देखें" - }, "viewAllQuotes": { "message": "सभी उद्धरण को देखें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 68b52556c3f6..6314d9ed3468 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..0c79dd8fc6fc 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" }, @@ -1649,9 +1632,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..61730b2bc325 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": "バイト" }, @@ -1900,13 +1879,6 @@ "functionType": { "message": "機能の種類" }, - "fundYourWallet": { - "message": "ウォレットへの入金" - }, - "fundYourWalletDescription": { - "message": "ウォレットに$1を追加して開始します。", - "description": "$1 is the token symbol" - }, "gas": { "message": "ガス" }, @@ -1990,14 +1962,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": "戻る" }, @@ -4858,9 +4822,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 +4975,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 +5960,6 @@ "viewActivity": { "message": "アクティビティを表示" }, - "viewAllDetails": { - "message": "すべての詳細の表示" - }, "viewAllQuotes": { "message": "すべてのクォートを表示" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index be1de55c51c7..05c04fbd17a9 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": "바이트" }, @@ -1900,13 +1879,6 @@ "functionType": { "message": "기능 유형" }, - "fundYourWallet": { - "message": "지갑에 자금 추가" - }, - "fundYourWalletDescription": { - "message": "지갑에 $1의 자금을 추가하여 시작하세요.", - "description": "$1 is the token symbol" - }, "gas": { "message": "가스" }, @@ -1990,14 +1962,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": "뒤로 가기" }, @@ -4858,9 +4822,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 +4975,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 +5960,6 @@ "viewActivity": { "message": "활동 보기" }, - "viewAllDetails": { - "message": "모든 세부 정보 보기" - }, "viewAllQuotes": { "message": "모든 견적 보기" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 1687cb7818f0..e6b4bc3e7811 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" }, @@ -1594,9 +1577,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..4c02a9dc223e 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..58d7f6ea8718 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" }, @@ -2335,9 +2318,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..f1e5d27589c5 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": "Байты" }, @@ -1900,13 +1879,6 @@ "functionType": { "message": "Тип функции" }, - "fundYourWallet": { - "message": "Пополните свой кошелек" - }, - "fundYourWalletDescription": { - "message": "Начните с добавления $1 в свой кошелек.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Газ" }, @@ -1990,14 +1962,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": "Назад" }, @@ -4858,9 +4822,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 +4975,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 +5960,6 @@ "viewActivity": { "message": "Смотреть активность" }, - "viewAllDetails": { - "message": "Смотреть все сведения" - }, "viewAllQuotes": { "message": "смотреть все котировки" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 61d8ff6e5d8c..76e91829fc2c 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..3b1899614d70 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..4bfcba6dac1f 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" }, @@ -1900,13 +1879,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 +1962,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" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..80a31d532482 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": "字节" }, @@ -1900,13 +1879,6 @@ "functionType": { "message": "功能类型" }, - "fundYourWallet": { - "message": "向您的钱包存入资金" - }, - "fundYourWalletDescription": { - "message": "将一些 $1 添加到您的钱包并开始使用", - "description": "$1 is the token symbol" - }, "gas": { "message": "燃料" }, @@ -1990,14 +1962,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": "返回" }, @@ -4858,9 +4822,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 +4975,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 +5960,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..7a7fdb68cf1f 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": "新增朋友和您信任的位址" }, @@ -1370,9 +1353,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/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/scripts/background.js b/app/scripts/background.js index ad6e3b6f22c2..bacb6adddf9f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -298,6 +298,9 @@ function maybeDetectPhishing(theController) { category: MetaMetricsEventCategory.Phishing, properties: { url: hostname, + referrer: { + url: hostname, + }, reason: blockReason, }, }); diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 76fb2386f1f6..3125016ea0b5 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: { @@ -233,6 +246,7 @@ export const SENTRY_BACKGROUND_STATE = { showNativeTokenAsMainBalance: true, petnamesEnabled: true, showConfirmationAdvancedDetails: true, + privacyMode: false, }, useExternalServices: false, selectedAddress: false, 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..64bc46132724 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -237,8 +237,6 @@ describe('MMIController', function () { messenger: mockMessenger, }), isEthSignEnabled: jest.fn(), - getAllState: jest.fn(), - getCurrentChainId: jest.fn(), }), appStateController: new AppStateController({ addUnlockListener: jest.fn(), diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 9c28ed7c43a0..74daf39e17ad 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -730,6 +730,7 @@ describe('preferences controller', () => { expect(controller.state.preferences).toStrictEqual({ autoLockTimeLimit: undefined, showExtensionInFullSizeView: false, + privacyMode: false, showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: null, @@ -749,6 +750,7 @@ describe('preferences controller', () => { order: 'dsc', sortCallback: 'stringNumeric', }, + tokenNetworkFilter: {}, }); }); @@ -764,6 +766,7 @@ describe('preferences controller', () => { 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: {}, }); }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index 536ec33b34eb..f6537952d651 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -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; }; @@ -214,12 +216,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, 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/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.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index 8977c00aa3d7..16317fef0380 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -7,7 +7,6 @@ import { BlockaidReason, BlockaidResultType, } from '../../../../shared/constants/security-provider'; -import { flushPromises } from '../../../../test/lib/timer-helpers'; import { mockNetworkState } from '../../../../test/stub/networks'; import { createPPOMMiddleware, PPOMMiddlewareRequest } from './ppom-middleware'; import { @@ -105,7 +104,6 @@ const createMiddleware = ( }; describe('PPOMMiddleware', () => { - const validateRequestWithPPOMMock = jest.mocked(validateRequestWithPPOM); const generateSecurityAlertIdMock = jest.mocked(generateSecurityAlertId); const handlePPOMErrorMock = jest.mocked(handlePPOMError); const isChainSupportedMock = jest.mocked(isChainSupported); @@ -114,7 +112,6 @@ describe('PPOMMiddleware', () => { beforeEach(() => { jest.resetAllMocks(); - validateRequestWithPPOMMock.mockResolvedValue(SECURITY_ALERT_RESPONSE_MOCK); generateSecurityAlertIdMock.mockReturnValue(SECURITY_ALERT_ID_MOCK); handlePPOMErrorMock.mockReturnValue(SECURITY_ALERT_RESPONSE_MOCK); isChainSupportedMock.mockResolvedValue(true); @@ -129,38 +126,13 @@ describe('PPOMMiddleware', () => { }; }); - it('updates alert response after validating request', async () => { + it('adds checking chain response to confirmation requests while validation is in progress', async () => { const updateSecurityAlertResponse = jest.fn(); const middlewareFunction = createMiddleware({ updateSecurityAlertResponse, }); - const req = { - ...REQUEST_MOCK, - method: 'eth_sendTransaction', - securityAlertResponse: undefined, - }; - - await middlewareFunction( - req, - { ...JsonRpcResponseStruct.TYPE }, - () => undefined, - ); - - await flushPromises(); - - expect(updateSecurityAlertResponse).toHaveBeenCalledTimes(1); - expect(updateSecurityAlertResponse).toHaveBeenCalledWith( - req.method, - SECURITY_ALERT_ID_MOCK, - SECURITY_ALERT_RESPONSE_MOCK, - ); - }); - - it('adds loading response to confirmation requests while validation is in progress', async () => { - const middlewareFunction = createMiddleware(); - const req: PPOMMiddlewareRequest<(string | { to: string })[]> = { ...REQUEST_MOCK, method: 'eth_sendTransaction', @@ -173,7 +145,9 @@ describe('PPOMMiddleware', () => { () => undefined, ); - expect(req.securityAlertResponse?.reason).toBe(BlockaidReason.inProgress); + expect(req.securityAlertResponse?.reason).toBe( + BlockaidReason.checkingChain, + ); expect(req.securityAlertResponse?.result_type).toBe( BlockaidResultType.Loading, ); @@ -197,50 +171,6 @@ describe('PPOMMiddleware', () => { expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); - it('does not do validation if unable to get the chainId from the network provider config', async () => { - isChainSupportedMock.mockResolvedValue(false); - const middlewareFunction = createMiddleware({ - chainId: null, - }); - - const req = { - ...REQUEST_MOCK, - method: 'eth_sendTransaction', - securityAlertResponse: undefined, - }; - - await middlewareFunction( - req, - { ...JsonRpcResponseStruct.TYPE }, - () => undefined, - ); - - expect(req.securityAlertResponse).toBeUndefined(); - expect(validateRequestWithPPOM).not.toHaveBeenCalled(); - }); - - it('does not do validation if user is not on a supported network', async () => { - isChainSupportedMock.mockResolvedValue(false); - const middlewareFunction = createMiddleware({ - chainId: '0x2', - }); - - const req = { - ...REQUEST_MOCK, - method: 'eth_sendTransaction', - securityAlertResponse: undefined, - }; - - await middlewareFunction( - req, - { ...JsonRpcResponseStruct.TYPE }, - () => undefined, - ); - - expect(req.securityAlertResponse).toBeUndefined(); - expect(validateRequestWithPPOM).not.toHaveBeenCalled(); - }); - it('does not do validation when request is not for confirmation method', async () => { const middlewareFunction = createMiddleware(); diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 7eb8dc0cc5a2..44c7a8a965c4 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -13,17 +13,16 @@ import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import { PreferencesController } from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state-controller'; -import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; +import { SECURITY_ALERT_RESPONSE_CHECKING_CHAIN } from '../../../../shared/constants/security-provider'; // eslint-disable-next-line import/no-restricted-paths import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; import { trace, TraceContext, TraceName } from '../../../../shared/lib/trace'; import { generateSecurityAlertId, handlePPOMError, - isChainSupported, validateRequestWithPPOM, } from './ppom-util'; -import { SecurityAlertResponse } from './types'; +import { SecurityAlertResponse, UpdateSecurityAlertResponse } from './types'; const CONFIRMATION_METHODS = Object.freeze([ 'eth_sendRawTransaction', @@ -64,11 +63,7 @@ export function createPPOMMiddleware< networkController: NetworkController, appStateController: AppStateController, accountsController: AccountsController, - updateSecurityAlertResponse: ( - method: string, - signatureAlertId: string, - securityAlertResponse: SecurityAlertResponse, - ) => void, + updateSecurityAlertResponse: UpdateSecurityAlertResponse, ) { return async ( req: PPOMMiddlewareRequest, @@ -86,12 +81,9 @@ export function createPPOMMiddleware< return; } - const isSupportedChain = await isChainSupported(chainId); - if ( !securityAlertsEnabled || - !CONFIRMATION_METHODS.includes(req.method) || - !isSupportedChain + !CONFIRMATION_METHODS.includes(req.method) ) { return; } @@ -123,27 +115,22 @@ export function createPPOMMiddleware< request: req, securityAlertId, chainId, - }).then((securityAlertResponse) => { - updateSecurityAlertResponse( - req.method, - securityAlertId, - securityAlertResponse, - ); + updateSecurityAlertResponse, }), ); - const loadingSecurityAlertResponse: SecurityAlertResponse = { - ...LOADING_SECURITY_ALERT_RESPONSE, + const securityAlertResponseCheckingChain: SecurityAlertResponse = { + ...SECURITY_ALERT_RESPONSE_CHECKING_CHAIN, securityAlertId, }; if (SIGNING_METHODS.includes(req.method)) { appStateController.addSignatureSecurityAlertResponse( - loadingSecurityAlertResponse, + securityAlertResponseCheckingChain, ); } - req.securityAlertResponse = loadingSecurityAlertResponse; + req.securityAlertResponse = securityAlertResponseCheckingChain; } catch (error) { req.securityAlertResponse = handlePPOMError( error, diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index ea62c3b88533..8acb6dd9788c 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -10,9 +10,12 @@ import { SignatureController, SignatureRequest, } from '@metamask/signature-controller'; +import { Hex } from '@metamask/utils'; import { BlockaidReason, BlockaidResultType, + LOADING_SECURITY_ALERT_RESPONSE, + SECURITY_ALERT_RESPONSE_CHAIN_NOT_SUPPORTED, SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { AppStateController } from '../../controllers/app-state-controller'; @@ -32,7 +35,7 @@ jest.mock('@metamask/transaction-controller', () => ({ const SECURITY_ALERT_ID_MOCK = '1234-5678'; const TRANSACTION_ID_MOCK = '123'; -const CHAIN_ID_MOCK = '0x1'; +const CHAIN_ID_MOCK = '0x1' as Hex; const REQUEST_MOCK = { method: 'eth_signTypedData_v4', @@ -45,6 +48,7 @@ const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { result_type: 'success', reason: 'success', source: SecurityAlertSource.Local, + securityAlertId: SECURITY_ALERT_ID_MOCK, }; const TRANSACTION_PARAMS_MOCK_1: TransactionParams = { @@ -110,6 +114,15 @@ describe('PPOM Utils', () => { ); let isSecurityAlertsEnabledMock: jest.SpyInstance; + const updateSecurityAlertResponseMock = jest.fn(); + + const validateRequestWithPPOMOptionsBase = { + request: REQUEST_MOCK, + securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, + updateSecurityAlertResponse: updateSecurityAlertResponseMock, + }; + beforeEach(() => { jest.resetAllMocks(); jest.spyOn(console, 'error').mockImplementation(() => undefined); @@ -119,7 +132,7 @@ describe('PPOM Utils', () => { }); describe('validateRequestWithPPOM', () => { - it('returns response from validation with PPOM instance via controller', async () => { + it('updates response from validation with PPOM instance via controller', async () => { const ppom = createPPOMMock(); const ppomController = createPPOMControllerMock(); @@ -129,23 +142,39 @@ describe('PPOM Utils', () => { (callback) => callback(ppom as any) as any, ); - const response = await validateRequestWithPPOM({ + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, ppomController, - request: REQUEST_MOCK, - securityAlertId: SECURITY_ALERT_ID_MOCK, - chainId: CHAIN_ID_MOCK, - }); - - expect(response).toStrictEqual({ - ...SECURITY_ALERT_RESPONSE_MOCK, - securityAlertId: SECURITY_ALERT_ID_MOCK, }); + expect(updateSecurityAlertResponseMock).toHaveBeenCalledWith( + REQUEST_MOCK.method, + SECURITY_ALERT_ID_MOCK, + { + ...SECURITY_ALERT_RESPONSE_MOCK, + securityAlertId: SECURITY_ALERT_ID_MOCK, + }, + ); expect(ppom.validateJsonRpc).toHaveBeenCalledTimes(1); expect(ppom.validateJsonRpc).toHaveBeenCalledWith(REQUEST_MOCK); }); - it('returns error response if validation with PPOM instance throws', async () => { + it('updates securityAlertResponse with loading state', async () => { + const ppomController = createPPOMControllerMock(); + + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, + ppomController, + }); + + expect(updateSecurityAlertResponseMock).toHaveBeenCalledWith( + REQUEST_MOCK.method, + SECURITY_ALERT_ID_MOCK, + LOADING_SECURITY_ALERT_RESPONSE, + ); + }); + + it('updates error response if validation with PPOM instance throws', async () => { const ppom = createPPOMMock(); const ppomController = createPPOMControllerMock(); @@ -157,37 +186,41 @@ describe('PPOM Utils', () => { callback(ppom as any) as any, ); - const response = await validateRequestWithPPOM({ + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, ppomController, - request: REQUEST_MOCK, - securityAlertId: SECURITY_ALERT_ID_MOCK, - chainId: CHAIN_ID_MOCK, }); - expect(response).toStrictEqual({ - result_type: BlockaidResultType.Errored, - reason: BlockaidReason.errored, - description: 'Test Error: Test error message', - }); + expect(updateSecurityAlertResponseMock).toHaveBeenCalledWith( + validateRequestWithPPOMOptionsBase.request.method, + SECURITY_ALERT_ID_MOCK, + { + result_type: BlockaidResultType.Errored, + reason: BlockaidReason.errored, + description: 'Test Error: Test error message', + }, + ); }); - it('returns error response if controller throws', async () => { + it('updates error response if controller throws', async () => { const ppomController = createPPOMControllerMock(); ppomController.usePPOM.mockRejectedValue(createErrorMock()); - const response = await validateRequestWithPPOM({ + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, ppomController, - request: REQUEST_MOCK, - securityAlertId: SECURITY_ALERT_ID_MOCK, - chainId: CHAIN_ID_MOCK, }); - expect(response).toStrictEqual({ - result_type: BlockaidResultType.Errored, - reason: BlockaidReason.errored, - description: 'Test Error: Test error message', - }); + expect(updateSecurityAlertResponseMock).toHaveBeenCalledWith( + validateRequestWithPPOMOptionsBase.request.method, + SECURITY_ALERT_ID_MOCK, + { + result_type: BlockaidResultType.Errored, + reason: BlockaidReason.errored, + description: 'Test Error: Test error message', + }, + ); }); it('normalizes request if method is eth_sendTransaction', async () => { @@ -209,10 +242,9 @@ describe('PPOM Utils', () => { }; await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, ppomController, request, - securityAlertId: SECURITY_ALERT_ID_MOCK, - chainId: CHAIN_ID_MOCK, }); expect(ppom.validateJsonRpc).toHaveBeenCalledTimes(1); @@ -226,6 +258,23 @@ describe('PPOM Utils', () => { TRANSACTION_PARAMS_MOCK_1, ); }); + + it('updates response indicating chain is not supported', async () => { + const ppomController = {} as PPOMController; + const CHAIN_ID_UNSUPPORTED_MOCK = '0x2'; + + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, + ppomController, + chainId: CHAIN_ID_UNSUPPORTED_MOCK, + }); + + expect(updateSecurityAlertResponseMock).toHaveBeenCalledWith( + validateRequestWithPPOMOptionsBase.request.method, + SECURITY_ALERT_ID_MOCK, + SECURITY_ALERT_RESPONSE_CHAIN_NOT_SUPPORTED, + ); + }); }); describe('generateSecurityAlertId', () => { @@ -318,10 +367,9 @@ describe('PPOM Utils', () => { const ppomController = createPPOMControllerMock(); await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, ppomController, request, - securityAlertId: SECURITY_ALERT_ID_MOCK, - chainId: CHAIN_ID_MOCK, }); expect(ppomController.usePPOM).not.toHaveBeenCalled(); @@ -345,10 +393,9 @@ describe('PPOM Utils', () => { .mockRejectedValue(new Error('Test Error')); await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, ppomController, request, - securityAlertId: SECURITY_ALERT_ID_MOCK, - chainId: CHAIN_ID_MOCK, }); expect(ppomController.usePPOM).toHaveBeenCalledTimes(1); diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 7662c364b651..fa1172ba01eb 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -11,12 +11,14 @@ import { SignatureController } from '@metamask/signature-controller'; import { BlockaidReason, BlockaidResultType, + LOADING_SECURITY_ALERT_RESPONSE, + SECURITY_ALERT_RESPONSE_CHAIN_NOT_SUPPORTED, SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import { AppStateController } from '../../controllers/app-state-controller'; -import { SecurityAlertResponse } from './types'; +import { SecurityAlertResponse, UpdateSecurityAlertResponse } from './types'; import { getSecurityAlertsAPISupportedChainIds, isSecurityAlertsAPIEnabled, @@ -37,18 +39,36 @@ type PPOMRequest = Omit & { method: typeof METHOD_SEND_TRANSACTION; params: [TransactionParams]; }; + export async function validateRequestWithPPOM({ ppomController, request, securityAlertId, chainId, + updateSecurityAlertResponse: updateSecurityResponse, }: { ppomController: PPOMController; request: JsonRpcRequest; securityAlertId: string; chainId: Hex; -}): Promise { + updateSecurityAlertResponse: UpdateSecurityAlertResponse; +}) { try { + if (!(await isChainSupported(chainId))) { + await updateSecurityResponse( + request.method, + securityAlertId, + SECURITY_ALERT_RESPONSE_CHAIN_NOT_SUPPORTED, + ); + return; + } + + await updateSecurityResponse( + request.method, + securityAlertId, + LOADING_SECURITY_ALERT_RESPONSE, + ); + const normalizedRequest = normalizePPOMRequest(request); const ppomResponse = isSecurityAlertsAPIEnabled() @@ -58,13 +78,13 @@ export async function validateRequestWithPPOM({ normalizedRequest, chainId, ); - - return { - ...ppomResponse, - securityAlertId, - }; + await updateSecurityResponse(request.method, securityAlertId, ppomResponse); } catch (error: unknown) { - return handlePPOMError(error, 'Error validating JSON RPC using PPOM: '); + await updateSecurityResponse( + request.method, + securityAlertId, + handlePPOMError(error, 'Error validating JSON RPC using PPOM: '), + ); } } @@ -97,12 +117,15 @@ export async function updateSecurityAlertResponse({ ); if (isSignatureRequest) { - appStateController.addSignatureSecurityAlertResponse(securityAlertResponse); + appStateController.addSignatureSecurityAlertResponse({ + ...securityAlertResponse, + securityAlertId, + }); } else { - transactionController.updateSecurityAlertResponse( - confirmation.id, - securityAlertResponse, - ); + transactionController.updateSecurityAlertResponse(confirmation.id, { + ...securityAlertResponse, + securityAlertId, + } as SecurityAlertResponse); } } 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/ppom/types.ts b/app/scripts/lib/ppom/types.ts index 6188d644aa12..57dd4a9e533d 100644 --- a/app/scripts/lib/ppom/types.ts +++ b/app/scripts/lib/ppom/types.ts @@ -10,3 +10,9 @@ export type SecurityAlertResponse = { securityAlertId?: string; source?: SecurityAlertSource; }; + +export type UpdateSecurityAlertResponse = ( + method: string, + securityAlertId: string, + securityAlertResponse: SecurityAlertResponse, +) => Promise; 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..3cdc14619b0d 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 @@ -1004,7 +998,7 @@ async function buildEventFragmentProperties({ } const isRedesignedConfirmationsDeveloperSettingEnabled = transactionMetricsRequest.getIsRedesignedConfirmationsDeveloperEnabled() || - Boolean(process.env.ENABLE_CONFIRMATION_REDESIGN); + process.env.ENABLE_CONFIRMATION_REDESIGN === 'true'; const isRedesignedTransactionsUserSettingEnabled = transactionMetricsRequest.getRedesignedTransactionsEnabled(); @@ -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/lib/transaction/util.test.ts b/app/scripts/lib/transaction/util.test.ts index 16077e0f08ed..fbbee025381b 100644 --- a/app/scripts/lib/transaction/util.test.ts +++ b/app/scripts/lib/transaction/util.test.ts @@ -16,7 +16,6 @@ import { BlockaidReason, BlockaidResultType, } from '../../../../shared/constants/security-provider'; -import { SecurityAlertResponse } from '../ppom/types'; import { flushPromises } from '../../../../test/lib/timer-helpers'; import { createMockInternalAccount } from '../../../../test/jest/mocks'; import { @@ -79,11 +78,6 @@ const TRANSACTION_REQUEST_MOCK: AddTransactionRequest = { internalAccounts: [], } as unknown as AddTransactionRequest; -const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { - result_type: BlockaidResultType.Malicious, - reason: BlockaidReason.maliciousDomain, -}; - function createTransactionControllerMock() { return { addTransaction: jest.fn(), @@ -406,10 +400,6 @@ describe('Transaction Utils', () => { describe('validates using security provider', () => { it('adds loading response to request options', async () => { - validateRequestWithPPOMMock.mockResolvedValue( - SECURITY_ALERT_RESPONSE_MOCK, - ); - await addTransaction({ ...request, securityAlertsEnabled: true, @@ -425,36 +415,13 @@ describe('Transaction Utils', () => { ).toHaveBeenCalledWith(TRANSACTION_PARAMS_MOCK, { ...TRANSACTION_OPTIONS_MOCK, securityAlertResponse: { - reason: BlockaidReason.inProgress, + reason: BlockaidReason.checkingChain, result_type: BlockaidResultType.Loading, securityAlertId: SECURITY_ALERT_ID_MOCK, }, }); }); - it('updates response after validation', async () => { - validateRequestWithPPOMMock.mockResolvedValue( - SECURITY_ALERT_RESPONSE_MOCK, - ); - - await addTransaction({ - ...request, - securityAlertsEnabled: true, - chainId: '0x1', - }); - - await flushPromises(); - - expect(request.updateSecurityAlertResponse).toHaveBeenCalledTimes(1); - expect(request.updateSecurityAlertResponse).toHaveBeenCalledWith( - 'eth_sendTransaction', - SECURITY_ALERT_ID_MOCK, - SECURITY_ALERT_RESPONSE_MOCK, - ); - - expect(validateRequestWithPPOMMock).toHaveBeenCalledTimes(1); - }); - it('unless blockaid is disabled', async () => { await addTransaction({ ...request, @@ -505,29 +472,6 @@ describe('Transaction Utils', () => { expect(validateRequestWithPPOMMock).toHaveBeenCalledTimes(0); }); - it('unless chain is not supported', async () => { - isChainSupportedMock.mockResolvedValue(false); - - await addTransaction({ - ...request, - securityAlertsEnabled: true, - chainId: '0xF', - }); - - expect( - request.transactionController.addTransaction, - ).toHaveBeenCalledTimes(1); - - expect( - request.transactionController.addTransaction, - ).toHaveBeenCalledWith( - TRANSACTION_PARAMS_MOCK, - TRANSACTION_OPTIONS_MOCK, - ); - - expect(validateRequestWithPPOMMock).toHaveBeenCalledTimes(0); - }); - it('unless transaction type is swap', async () => { const swapRequest = { ...request }; swapRequest.transactionOptions.type = TransactionType.swap; diff --git a/app/scripts/lib/transaction/util.ts b/app/scripts/lib/transaction/util.ts index 8b71e33119f8..0bbf93afd8a8 100644 --- a/app/scripts/lib/transaction/util.ts +++ b/app/scripts/lib/transaction/util.ts @@ -16,12 +16,14 @@ import { PPOMController } from '@metamask/ppom-validator'; import { generateSecurityAlertId, handlePPOMError, - isChainSupported, validateRequestWithPPOM, } from '../ppom/ppom-util'; -import { SecurityAlertResponse } from '../ppom/types'; import { - LOADING_SECURITY_ALERT_RESPONSE, + SecurityAlertResponse, + UpdateSecurityAlertResponse, +} from '../ppom/types'; +import { + SECURITY_ALERT_RESPONSE_CHECKING_CHAIN, SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES, } from '../../../../shared/constants/security-provider'; import { endTrace, TraceName } from '../../../../shared/lib/trace'; @@ -38,11 +40,7 @@ type BaseAddTransactionRequest = { selectedAccount: InternalAccount; transactionParams: TransactionParams; transactionController: TransactionController; - updateSecurityAlertResponse: ( - method: string, - securityAlertId: string, - securityAlertResponse: SecurityAlertResponse, - ) => void; + updateSecurityAlertResponse: UpdateSecurityAlertResponse; userOperationController: UserOperationController; internalAccounts: InternalAccount[]; }; @@ -239,18 +237,12 @@ async function validateSecurity(request: AddTransactionRequest) { const { type } = transactionOptions; - const isCurrentChainSupported = await isChainSupported(chainId); - const typeIsExcludedFromPPOM = SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES.includes( type as TransactionType, ); - if ( - !securityAlertsEnabled || - !isCurrentChainSupported || - typeIsExcludedFromPPOM - ) { + if (!securityAlertsEnabled || typeIsExcludedFromPPOM) { return; } @@ -290,21 +282,16 @@ async function validateSecurity(request: AddTransactionRequest) { request: ppomRequest, securityAlertId, chainId, - }).then((securityAlertResponse) => { - updateSecurityAlertResponse( - ppomRequest.method, - securityAlertId, - securityAlertResponse, - ); + updateSecurityAlertResponse, }); - const loadingSecurityAlertResponse: SecurityAlertResponse = { - ...LOADING_SECURITY_ALERT_RESPONSE, + const securityAlertResponseCheckingChain: SecurityAlertResponse = { + ...SECURITY_ALERT_RESPONSE_CHECKING_CHAIN, securityAlertId, }; request.transactionOptions.securityAlertResponse = - loadingSecurityAlertResponse; + securityAlertResponseCheckingChain; } catch (error) { handlePPOMError(error, 'Error validating JSON RPC using PPOM: '); } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4284a2614a9d..a275c4d17e5a 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 { @@ -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, @@ -1996,11 +1990,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, }); @@ -2128,7 +2120,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({ @@ -3952,6 +3944,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,10 +4009,9 @@ export default class MetamaskController extends EventEmitter { ), // CurrencyRateController - currencyRateStartPollingByNetworkClientId: - currencyRateController.startPollingByNetworkClientId.bind( - currencyRateController, - ), + currencyRateStartPolling: currencyRateController.startPolling.bind( + currencyRateController, + ), currencyRateStopPollingByPollingToken: currencyRateController.stopPollingByPollingToken.bind( currencyRateController, @@ -6533,10 +6529,12 @@ export default class MetamaskController extends EventEmitter { ); }, getRedesignedConfirmationsEnabled: () => { - return this.preferencesController.getRedesignedConfirmationsEnabled; + return this.preferencesController.state.preferences + .redesignedConfirmationsEnabled; }, getRedesignedTransactionsEnabled: () => { - return this.preferencesController.getRedesignedTransactionsEnabled; + return this.preferencesController.state.preferences + .redesignedTransactionsEnabled; }, getMethodData: (data) => { if (!data) { @@ -6685,12 +6683,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/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..824e08e32167 100644 --- a/builds.yml +++ b/builds.yml @@ -173,6 +173,12 @@ env: - SENTRY_MMI_DSN: '' + ### + # Storybook + ### + - STORYBOOK: false + - INFURA_STORYBOOK_PROJECT_ID + ### # Notifications Feature ### @@ -267,12 +273,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/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..ac719964d896 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, @@ -705,14 +674,14 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>@metamask/rpc-errors": true, + "@metamask/assets-controllers>@metamask/utils": 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/utils": true, + "@metamask/rpc-errors": true, "bn.js": true, "cockatiel": true, "ethers>@ethersproject/address": true, @@ -733,10 +702,19 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/rpc-errors": { + "@metamask/assets-controllers>@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/base-controller": { @@ -863,70 +841,13 @@ "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/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-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": { - "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>async-mutex": { - "globals": { - "setTimeout": true - }, - "packages": { - "@swc/helpers>tslib": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -966,14 +887,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, @@ -1315,24 +1241,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": { @@ -1389,6 +1315,17 @@ "semver": true } }, + "@metamask/json-rpc-middleware-stream": { + "globals": { + "console.warn": true, + "setTimeout": true + }, + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1528,7 +1465,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": { @@ -1810,6 +1753,7 @@ "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, + "@metamask/utils": true, "bignumber.js": true, "loglevel": true, "uuid": true @@ -2081,9 +2025,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": { @@ -2367,16 +2323,57 @@ "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/utils": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, + "@metamask/signature-controller>@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/signature-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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2390,9 +2387,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 +2397,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 +2411,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 +2418,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 +2435,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 +2471,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 +2483,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 @@ -2546,58 +2545,6 @@ "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": { - "globals": { - "console": 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/smart-transactions-controller>bignumber.js": { "globals": { "crypto": true, @@ -2614,11 +2561,11 @@ "setTimeout": true }, "packages": { + "@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/snaps-controllers>@metamask/permission-controller": true, "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, @@ -2658,17 +2605,6 @@ "@metamask/utils": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true - } - }, "@metamask/snaps-controllers>@metamask/permission-controller": { "globals": { "console.error": true @@ -3022,9 +2958,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 +2978,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, @@ -4077,59 +4025,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 +4052,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 +4118,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4247,33 +4139,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 +4480,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 +4615,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 +5131,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 +5358,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..ac719964d896 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, @@ -705,14 +674,14 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>@metamask/rpc-errors": true, + "@metamask/assets-controllers>@metamask/utils": 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/utils": true, + "@metamask/rpc-errors": true, "bn.js": true, "cockatiel": true, "ethers>@ethersproject/address": true, @@ -733,10 +702,19 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/rpc-errors": { + "@metamask/assets-controllers>@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/base-controller": { @@ -863,70 +841,13 @@ "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/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-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": { - "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>async-mutex": { - "globals": { - "setTimeout": true - }, - "packages": { - "@swc/helpers>tslib": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -966,14 +887,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, @@ -1315,24 +1241,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": { @@ -1389,6 +1315,17 @@ "semver": true } }, + "@metamask/json-rpc-middleware-stream": { + "globals": { + "console.warn": true, + "setTimeout": true + }, + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1528,7 +1465,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": { @@ -1810,6 +1753,7 @@ "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, + "@metamask/utils": true, "bignumber.js": true, "loglevel": true, "uuid": true @@ -2081,9 +2025,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": { @@ -2367,16 +2323,57 @@ "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/utils": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, + "@metamask/signature-controller>@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/signature-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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2390,9 +2387,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 +2397,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 +2411,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 +2418,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 +2435,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 +2471,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 +2483,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 @@ -2546,58 +2545,6 @@ "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": { - "globals": { - "console": 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/smart-transactions-controller>bignumber.js": { "globals": { "crypto": true, @@ -2614,11 +2561,11 @@ "setTimeout": true }, "packages": { + "@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/snaps-controllers>@metamask/permission-controller": true, "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, @@ -2658,17 +2605,6 @@ "@metamask/utils": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true - } - }, "@metamask/snaps-controllers>@metamask/permission-controller": { "globals": { "console.error": true @@ -3022,9 +2958,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 +2978,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, @@ -4077,59 +4025,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 +4052,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 +4118,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4247,33 +4139,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 +4480,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 +4615,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 +5131,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 +5358,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..ac719964d896 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, @@ -705,14 +674,14 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>@metamask/rpc-errors": true, + "@metamask/assets-controllers>@metamask/utils": 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/utils": true, + "@metamask/rpc-errors": true, "bn.js": true, "cockatiel": true, "ethers>@ethersproject/address": true, @@ -733,10 +702,19 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/rpc-errors": { + "@metamask/assets-controllers>@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/base-controller": { @@ -863,70 +841,13 @@ "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/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-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": { - "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>async-mutex": { - "globals": { - "setTimeout": true - }, - "packages": { - "@swc/helpers>tslib": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -966,14 +887,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, @@ -1315,24 +1241,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": { @@ -1389,6 +1315,17 @@ "semver": true } }, + "@metamask/json-rpc-middleware-stream": { + "globals": { + "console.warn": true, + "setTimeout": true + }, + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1528,7 +1465,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": { @@ -1810,6 +1753,7 @@ "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, + "@metamask/utils": true, "bignumber.js": true, "loglevel": true, "uuid": true @@ -2081,9 +2025,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": { @@ -2367,16 +2323,57 @@ "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/utils": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, + "@metamask/signature-controller>@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/signature-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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2390,9 +2387,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 +2397,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 +2411,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 +2418,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 +2435,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 +2471,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 +2483,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 @@ -2546,58 +2545,6 @@ "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": { - "globals": { - "console": 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/smart-transactions-controller>bignumber.js": { "globals": { "crypto": true, @@ -2614,11 +2561,11 @@ "setTimeout": true }, "packages": { + "@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/snaps-controllers>@metamask/permission-controller": true, "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, @@ -2658,17 +2605,6 @@ "@metamask/utils": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true - } - }, "@metamask/snaps-controllers>@metamask/permission-controller": { "globals": { "console.error": true @@ -3022,9 +2958,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 +2978,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, @@ -4077,59 +4025,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 +4052,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 +4118,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4247,33 +4139,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 +4480,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 +4615,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 +5131,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 +5358,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..1b4e98aae177 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, @@ -797,14 +766,14 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, - "@metamask/assets-controllers>@metamask/rpc-errors": true, + "@metamask/assets-controllers>@metamask/utils": 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/utils": true, + "@metamask/rpc-errors": true, "bn.js": true, "cockatiel": true, "ethers>@ethersproject/address": true, @@ -825,10 +794,19 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/rpc-errors": { + "@metamask/assets-controllers>@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/base-controller": { @@ -955,70 +933,13 @@ "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/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-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": { - "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>async-mutex": { - "globals": { - "setTimeout": true - }, - "packages": { - "@swc/helpers>tslib": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1058,14 +979,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, @@ -1407,24 +1333,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": { @@ -1481,6 +1407,17 @@ "semver": true } }, + "@metamask/json-rpc-middleware-stream": { + "globals": { + "console.warn": true, + "setTimeout": true + }, + "packages": { + "@metamask/safe-event-emitter": true, + "@metamask/utils": true, + "readable-stream": true + } + }, "@metamask/keyring-api": { "globals": { "URL": true @@ -1620,7 +1557,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": { @@ -1902,6 +1845,7 @@ "@metamask/notification-services-controller>@contentful/rich-text-html-renderer": true, "@metamask/notification-services-controller>firebase": true, "@metamask/profile-sync-controller": true, + "@metamask/utils": true, "bignumber.js": true, "loglevel": true, "uuid": true @@ -2173,9 +2117,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": { @@ -2459,16 +2415,57 @@ "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/utils": true, + "@metamask/signature-controller>@metamask/eth-sig-util": true, + "@metamask/signature-controller>@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/signature-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/smart-transactions-controller": { "globals": { "URLSearchParams": true, @@ -2482,9 +2479,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 +2489,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 +2503,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 +2510,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 +2527,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 +2563,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 +2575,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 @@ -2638,58 +2637,6 @@ "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": { - "globals": { - "console": 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/smart-transactions-controller>bignumber.js": { "globals": { "crypto": true, @@ -2706,11 +2653,11 @@ "setTimeout": true }, "packages": { + "@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/snaps-controllers>@metamask/permission-controller": true, "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, @@ -2750,17 +2697,6 @@ "@metamask/utils": true } }, - "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "@metamask/safe-event-emitter": true, - "@metamask/utils": true, - "readable-stream": true - } - }, "@metamask/snaps-controllers>@metamask/permission-controller": { "globals": { "console.error": true @@ -3114,9 +3050,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 +3070,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, @@ -4169,59 +4117,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 +4144,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 +4210,6 @@ "define": true } }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, "eth-lattice-keyring>gridplus-sdk>borc": { "globals": { "console": true @@ -4339,33 +4231,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 +4572,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 +4707,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 +5223,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 +5426,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..5a607452526c 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": { + "@babel/preset-env>@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "packages": { - "@babel/preset-env>@babel/helper-plugin-utils": true - } - }, - "@babel/preset-env>@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "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 } }, @@ -6328,24 +6237,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 +6281,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 +6291,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 +6329,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 +6605,8 @@ } }, "postcss>picocolors": { - "builtin": { - "tty.isatty": true - }, "globals": { - "process.argv.includes": true, - "process.env": true, - "process.platform": true + "process": true } }, "postcss>source-map-js": { diff --git a/package.json b/package.json index 594647671c7c..f7900e85d536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.6.1", + "version": "12.7.0", "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", @@ -231,26 +235,8 @@ "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", "@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", @@ -265,13 +251,14 @@ "@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" + "@metamask/snaps-utils@npm:^8.4.1": "patch:@metamask/snaps-utils@npm%3A8.4.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch", + "@metamask/snaps-utils@npm:^8.3.0": "patch:@metamask/snaps-utils@npm%3A8.4.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch", + "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.4.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch", + "@metamask/snaps-utils@npm:^8.4.0": "patch:@metamask/snaps-utils@npm%3A8.4.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch", + "@metamask/snaps-utils@npm:^7.4.0": "patch:@metamask/snaps-utils@npm%3A8.4.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch" }, "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 +272,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 +291,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%3A41.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-41.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 +315,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,30 +326,31 @@ "@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/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.4.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.4.1-90481bac4b.patch", "@metamask/transaction-controller": "^38.3.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", @@ -406,7 +395,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 +443,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,7 +468,7 @@ "@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", @@ -619,6 +607,7 @@ "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 +617,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", @@ -685,6 +675,7 @@ "watchify": "^4.0.0", "webextension-polyfill": "^0.8.0", "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.3", "ws": "^8.17.1", "yaml": "^2.4.1", @@ -702,17 +693,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 +706,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 +718,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,7 +749,8 @@ "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" diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 41b04a9b5210..589504ea2cc7 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -48,6 +48,8 @@ "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", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 8107a1040127..615c562e21ad 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 = { @@ -223,6 +235,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 +276,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 +317,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 +337,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 +377,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 +416,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 +448,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 +458,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 { @@ -601,6 +691,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/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..64d330b73b2c 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -147,6 +147,7 @@ export const CHAIN_IDS = { NUMBERS: '0x290b', SEI: '0x531', APE_TESTNET: '0x8157', + APE_MAINNET: '0x8173', BERACHAIN: '0x138d5', METACHAIN_ONE: '0x1b6e6', ARBITRUM_SEPOLIA: '0x66eee', @@ -207,6 +208,7 @@ export const CHAINLIST_CHAIN_IDS_MAP = { ZORA_MAINNET: '0x76adf1', FILECOIN: '0x13a', NUMBERS: '0x290b', + APE: '0x8173', } as const; // To add a deprecation warning to a network, add it to the array @@ -371,6 +373,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 +419,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 +453,7 @@ export const NUMBERS_MAINNET_IMAGE_URL = './images/numbers-mainnet.svg'; export const NUMBERS_TOKEN_IMAGE_URL = './images/numbers-token.png'; export const SEI_IMAGE_URL = './images/sei.svg'; export const NEAR_IMAGE_URL = './images/near.svg'; -export const APE_TESTNET_IMAGE_URL = './images/ape.svg'; +export const APE_IMAGE_URL = './images/ape.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -564,6 +568,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,7 +787,8 @@ 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, @@ -817,6 +823,7 @@ 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, } as const; export const INFURA_BLOCKED_KEY = 'countryBlocked'; @@ -932,6 +939,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..2864fe0339a9 100644 --- a/shared/constants/security-provider.ts +++ b/shared/constants/security-provider.ts @@ -32,7 +32,7 @@ export enum BlockaidReason { approvalFarming = 'approval_farming', /** Malicious signature on Blur order */ blurFarming = 'blur_farming', - /** A known malicous site invoked that transaction */ + /** A known malicious site invoked that transaction */ maliciousDomain = 'malicious_domain', /** Malicious signature on a Permit order */ permitFarming = 'permit_farming', @@ -57,6 +57,8 @@ export enum BlockaidReason { errored = 'Error', notApplicable = 'NotApplicable', inProgress = 'validation_in_progress', + checkingChain = 'CheckingChain', + chainNotSupported = 'ChainNotSupported', } export enum BlockaidResultType { @@ -117,6 +119,17 @@ export const LOADING_SECURITY_ALERT_RESPONSE: SecurityAlertResponse = { reason: BlockaidReason.inProgress, }; +export const SECURITY_ALERT_RESPONSE_CHECKING_CHAIN: SecurityAlertResponse = { + result_type: BlockaidResultType.Loading, + reason: BlockaidReason.checkingChain, +}; + +export const SECURITY_ALERT_RESPONSE_CHAIN_NOT_SUPPORTED: SecurityAlertResponse = + { + result_type: BlockaidResultType.Benign, + reason: BlockaidReason.chainNotSupported, + }; + export enum SecurityAlertSource { /** Validation performed remotely using the Security Alerts API. */ API = 'api', 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/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/mock-state.json b/test/data/mock-state.json index 654e915a1305..2865478912f3 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -420,6 +420,7 @@ "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", "id": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3", "metadata": { + "importTime": 0, "name": "Test Account", "keyring": { "type": "HD Key Tree" @@ -439,6 +440,7 @@ "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", "id": "07c2cfec-36c9-46c4-8115-3836d3ac9047", "metadata": { + "importTime": 0, "name": "Test Account 2", "keyring": { "type": "HD Key Tree" @@ -458,6 +460,7 @@ "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", "id": "15e69915-2a1a-4019-93b3-916e11fd432f", "metadata": { + "importTime": 0, "name": "Ledger Hardware 2", "keyring": { "type": "Ledger Hardware" @@ -477,6 +480,7 @@ "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823", "id": "784225f4-d30b-4e77-a900-c8bbce735b88", "metadata": { + "importTime": 0, "name": "Test Account 3", "keyring": { "type": "HD Key Tree" @@ -496,6 +500,7 @@ "address": "0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281", "id": "694225f4-d30b-4e77-a900-c8bbce735b42", "metadata": { + "importTime": 0, "name": "Test Account 4", "keyring": { "type": "Custody test" @@ -515,11 +520,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/fixture-builder.js b/test/e2e/fixture-builder.js index d73b959946c2..4d7e1873bff4 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,6 +58,7 @@ function onboardingFixture() { }), providerConfig: { id: 'networkConfigurationId' }, }, + NotificationServicesController: {}, PreferencesController: { advancedGasFee: {}, currentLocale: 'en', @@ -73,6 +75,7 @@ function onboardingFixture() { hideZeroBalanceTokens: false, showExtensionInFullSizeView: false, showFiatInTestnets: false, + privacyMode: false, showTestNetworks: false, smartTransactionsOptInStatus: false, showNativeTokenAsMainBalance: true, @@ -138,6 +141,7 @@ function onboardingFixture() { }, }, }, + UserStorageController: {}, TokensController: { allDetectedTokens: {}, allIgnoredTokens: {}, @@ -445,12 +449,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/helpers.js b/test/e2e/helpers.js index 6d2ccebeb7c7..5eaf14b8360b 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -435,6 +435,7 @@ const completeImportSRPOnboardingFlowWordByWord = async ( await driver.clickElement('[data-testid="onboarding-import-wallet"]'); // metrics + await driver.clickElement('[data-testid="metametrics-no-thanks"]'); // import with recovery phrase, word by word @@ -564,9 +565,40 @@ const onboardingPinExtension = async (driver) => { await driver.clickElement('[data-testid="pin-extension-done"]'); }; -const onboardingCompleteWalletCreationWithOptOut = async (driver) => { +/** + * 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 +606,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,15 +662,30 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await onboardingPinExtension(driver); }; +/** + * 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); }; const completeCreateNewWalletOnboardingFlow = async (driver, password) => { @@ -630,45 +697,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 +909,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 +929,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({ @@ -1281,8 +1311,6 @@ module.exports = { closeSRPReveal, tapAndHoldToRevealSRP, createDownloadFolder, - importWrongSRPOnboardingFlow, - testSRPDropdownIterations, openDapp, openDappConnectionsPage, createDappTransaction, @@ -1313,6 +1341,7 @@ module.exports = { onboardingCreatePassword, onboardingRevealAndConfirmSRP, onboardingCompleteWalletCreation, + onboardingCompleteWalletCreationWithOptOut, onboardingPinExtension, assertInAnyOrder, genRandInitBal, 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..85636fcb9089 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, +} = 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, + }; + }); + + 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..b5fda9e0c276 --- /dev/null +++ b/test/e2e/page-objects/flows/onboarding.flow.ts @@ -0,0 +1,68 @@ +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'; + +export const createNewWalletOnboardingFlow = async (driver: Driver) => { + 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(); + + const secureWalletPage = new SecureWalletPage(driver); + await secureWalletPage.check_pageIsLoaded(); + await secureWalletPage.revealAndConfirmSRP(); +}; + +export const importSRPOnboardingFlow = async (driver: Driver) => { + 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(); + await onboardingSrpPage.fillSrp(); + await onboardingSrpPage.clickConfirmButton(); + + const onboardingPasswordPage = new OnboardingPasswordPage(driver); + await onboardingPasswordPage.check_pageIsLoaded(); + await onboardingPasswordPage.createImportedWalletPassword(); +}; + +export const completeCreateNewWalletOnboardingFlow = async (driver: Driver) => { + console.log('start to complete create new wallet onboarding flow '); + await createNewWalletOnboardingFlow(driver); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_congratulationsMessageIsDisplayed(); + await onboardingCompletePage.completeOnboarding(); +}; + +export const completeImportSRPOnboardingFlow = async (driver: Driver) => { + console.log('start to complete import srp onboarding flow '); + await importSRPOnboardingFlow(driver); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); + await onboardingCompletePage.completeOnboarding(); +}; diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 7218c727a929..ae02973b6ae5 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -20,9 +20,15 @@ class AccountListPage { private readonly addAccountConfirmButton = '[data-testid="submit-add-account-with-name"]'; + private readonly importAccountConfirmButton = + '[data-testid="import-account-confirm-button"]'; + 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', @@ -54,6 +60,8 @@ class AccountListPage { private readonly saveAccountLabelButton = '[data-testid="save-account-label-input"]'; + private readonly importAccountPrivateKeyInput = '#private-key-box'; + constructor(driver: Driver) { this.driver = driver; } @@ -86,6 +94,34 @@ class AccountListPage { ); } + /** + * 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); + await this.driver.clickElementAndWaitToDisappear( + this.addAccountConfirmButton, + ); + } + + /** + * Adds a new account with a custom label. + * + * @param privateKey - Private key of the account + */ + async addNewImportedAccount(privateKey: 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); + await this.driver.clickElementAndWaitToDisappear( + this.importAccountConfirmButton, + ); + } + /** * Changes the label of the current account. * @@ -227,6 +263,25 @@ class AccountListPage { 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); + } } export default AccountListPage; 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..2c399a4118d8 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/select-network.ts @@ -0,0 +1,79 @@ +import { Driver } from '../../../webdriver/driver'; + +class SelectNetwork { + private driver: Driver; + + private networkName: string | undefined; + + private readonly addNetworkButton = { + tag: 'button', + text: 'Add a custom network', + }; + + private readonly closeButton = 'button[aria-label="Close"]'; + + private readonly searchInput = + '[data-testid="network-redesign-modal-search-input"]'; + + private readonly selectNetworkMessage = { + text: 'Select a network', + 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 selectNetworkName(networkName: string): Promise { + console.log(`Click ${networkName}`); + this.networkName = `[data-testid="${networkName}"]`; + await this.driver.clickElementAndWaitToDisappear(this.networkName); + } + + async addNewNetwork(): Promise { + console.log('Click Add network'); + await this.driver.clickElement(this.addNetworkButton); + } + + async clickCloseButton(): Promise { + console.log('Click Close Button'); + await this.driver.clickElementAndWaitToDisappear(this.closeButton); + } + + async toggleShowTestNetwork(): Promise { + console.log('Toggle show test network in select network dialog'); + await this.driver.clickElement(this.toggleButton); + } + + async fillNetworkSearchInput(networkName: string): Promise { + console.log(`Fill network search input with ${networkName}`); + await this.driver.fill(this.searchInput, networkName); + } + + async clickAddButton(): Promise { + console.log('Click Add Button'); + await this.driver.clickElementAndWaitToDisappear( + '[data-testid="test-add-button"]', + ); + } +} + +export default SelectNetwork; 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..7c322b0f2cbb 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -11,8 +11,17 @@ 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 completedTransactions = '[data-testid="activity-list-item"]'; private readonly confirmedTransactions = { @@ -60,6 +69,37 @@ class HomePage { await this.driver.clickElement(this.activityTab); } + async check_basicFunctionalityOffWarnigMessageIsDisplayed(): Promise { + console.log( + 'Check if basic functionality off warning message is displayed on homepage', + ); + await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); + } + + async goToNFTList(): Promise { + console.log(`Open NFT tab on homepage`); + await this.driver.clickElement(this.nftTab); + } + + async clickNFTIconOnActivityList() { + await this.driver.clickElement(this.nftIconOnActivityList); + } + + /** + * Checks if the toaster message for adding a network is displayed on the homepage. + * + * @param networkName - The name of the network that was added. + */ + async check_addNetworkMessageIsDisplayed(networkName: string): Promise { + console.log( + `Check the toaster message for adding network ${networkName} is displayed on homepage`, + ); + await this.driver.waitForSelector({ + tag: 'h6', + text: `“${networkName}” was successfully added!`, + }); + } + /** * 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. 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..81c2d21aceb6 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts @@ -0,0 +1,112 @@ +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 newPassword - The new password to create. Defaults to WALLET_PASSWORD. + * @param confirmPassword - The confirm password to create. Defaults to WALLET_PASSWORD. + */ + async createImportedWalletPassword( + newPassword: string = WALLET_PASSWORD, + confirmPassword: string = WALLET_PASSWORD, + ): Promise { + console.log('Create password for new imported wallet'); + await this.fillWalletPassword(newPassword, confirmPassword); + await this.driver.clickElementAndWaitToDisappear(this.importWalletButton); + } + + /** + * Create a password for new created wallet + * + * @param newPassword - The new password to create. Defaults to WALLET_PASSWORD. + * @param confirmPassword - The confirm password to create. Defaults to WALLET_PASSWORD. + */ + async createWalletPassword( + newPassword: string = WALLET_PASSWORD, + confirmPassword: string = WALLET_PASSWORD, + ): Promise { + console.log('Create password for new created wallet'); + await this.fillWalletPassword(newPassword, confirmPassword); + 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..dac2ab447710 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts @@ -0,0 +1,194 @@ +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.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + + /** + * 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, + ); + } + + 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); + } + + /** + * 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.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + + /** + * 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.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } +} + +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..da3e74153c67 --- /dev/null +++ b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts @@ -0,0 +1,105 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../../webdriver/driver'; +import { TEST_SEED_PHRASE } from '../../../helpers'; + +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 TEST_SEED_PHRASE. + */ + async fillSrp(seedPhrase: string = TEST_SEED_PHRASE): Promise { + await this.driver.pasteIntoField(this.srpWord0, seedPhrase); + } + + 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..60ffd86c6cdd 100644 --- a/test/e2e/page-objects/pages/send/send-token-page.ts +++ b/test/e2e/page-objects/pages/send/send-token-page.ts @@ -9,6 +9,8 @@ class SendTokenPage { private inputAmount: string; + private inputNFTAmount: string; + private scanButton: string; private continueButton: object; @@ -26,6 +28,7 @@ class SendTokenPage { constructor(driver: Driver) { this.driver = driver; this.inputAmount = '[data-testid="currency-input"]'; + this.inputNFTAmount = '[data-testid="nft-input"]'; this.inputRecipient = '[data-testid="ens-input"]'; this.scanButton = '[data-testid="ens-qr-scan-button"]'; this.ensResolvedName = @@ -79,6 +82,10 @@ class SendTokenPage { ); } + async fillNFTAmount(amount: string) { + await this.driver.pasteIntoField(this.inputNFTAmount, amount); + } + async goToNextScreen(): Promise { await this.driver.clickElement(this.continueButton); } 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/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/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/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/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts index fc8a6d0ab240..328ad7811e1b 100644 --- a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts +++ b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts @@ -10,6 +10,10 @@ import { withRedesignConfirmationFixtures, } from '../helpers'; import { TestSuiteArguments } from '../transactions/shared'; +import { + BlockaidReason, + BlockaidResultType, +} from '../../../../../shared/constants/security-provider'; import { assertSignatureRejectedMetrics, openDappAndTriggerSignature, @@ -80,6 +84,8 @@ describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this alert_visualized: [], alert_visualized_count: 0, }, + securityAlertReason: BlockaidReason.notApplicable, + securityAlertResponse: BlockaidResultType.NotApplicable, }); }, mockSignatureRejected, @@ -130,6 +136,8 @@ describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this alert_visualized: ['requestFrom'], alert_visualized_count: 1, }, + securityAlertReason: BlockaidReason.notApplicable, + securityAlertResponse: BlockaidResultType.NotApplicable, }); }, mockSignatureRejected, 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..27cc92fe27cf 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -8,6 +8,10 @@ import { unlockWallet, } from '../../../helpers'; import { Driver } from '../../../webdriver/driver'; +import { + BlockaidReason, + BlockaidResultType, +} from '../../../../../shared/constants/security-provider'; export const WALLET_ADDRESS = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; export const WALLET_ETH_BALANCE = '25'; @@ -30,6 +34,8 @@ type AssertSignatureMetricsOptions = { location?: string; expectedProps?: Record; withAnonEvents?: boolean; + securityAlertReason?: string; + securityAlertResponse?: string; }; type SignatureEventProperty = { @@ -39,7 +45,7 @@ type SignatureEventProperty = { environment_type: 'background'; locale: 'en'; security_alert_reason: string; - security_alert_response: 'NotApplicable'; + security_alert_response: string; signature_type: string; eip712_primary_type?: string; ui_customizations?: string[]; @@ -58,11 +64,15 @@ const signatureAnonProperties = { * @param signatureType * @param primaryType * @param uiCustomizations + * @param securityAlertReason + * @param securityAlertResponse */ function getSignatureEventProperty( signatureType: string, primaryType: string, uiCustomizations: string[], + securityAlertReason: string = BlockaidReason.checkingChain, + securityAlertResponse: string = BlockaidResultType.Loading, ): SignatureEventProperty { const signatureEventProperty: SignatureEventProperty = { account_type: 'MetaMask', @@ -71,8 +81,8 @@ function getSignatureEventProperty( chain_id: '0x539', environment_type: 'background', locale: 'en', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', + security_alert_reason: securityAlertReason, + security_alert_response: securityAlertResponse, ui_customizations: uiCustomizations, }; @@ -89,15 +99,15 @@ function assertSignatureRequestedMetrics( signatureEventProperty: SignatureEventProperty, withAnonEvents = false, ) { - assertEventPropertiesMatch(events, 'Signature Requested', { - ...signatureEventProperty, - security_alert_reason: 'NotApplicable', - }); + assertEventPropertiesMatch( + events, + 'Signature Requested', + signatureEventProperty, + ); if (withAnonEvents) { assertEventPropertiesMatch(events, 'Signature Requested Anon', { ...signatureEventProperty, - security_alert_reason: 'NotApplicable', ...signatureAnonProperties, }); } @@ -110,12 +120,16 @@ export async function assertSignatureConfirmedMetrics({ primaryType = '', uiCustomizations = ['redesigned_confirmation'], withAnonEvents = false, + securityAlertReason, + securityAlertResponse, }: AssertSignatureMetricsOptions) { const events = await getEventPayloads(driver, mockedEndpoints); const signatureEventProperty = getSignatureEventProperty( signatureType, primaryType, uiCustomizations, + securityAlertReason, + securityAlertResponse, ); assertSignatureRequestedMetrics( @@ -147,12 +161,16 @@ export async function assertSignatureRejectedMetrics({ location, expectedProps = {}, withAnonEvents = false, + securityAlertReason, + securityAlertResponse, }: AssertSignatureMetricsOptions) { const events = await getEventPayloads(driver, mockedEndpoints); const signatureEventProperty = getSignatureEventProperty( signatureType, primaryType, uiCustomizations, + securityAlertReason, + securityAlertResponse, ); assertSignatureRequestedMetrics( @@ -200,14 +218,44 @@ function assertEventPropertiesMatch( expectedProperties: object, ) { const event = events.find((e) => e.event === eventName); + + const actualProperties = { ...event.properties }; + const expectedProps = { ...expectedProperties }; + + compareSecurityAlertResponse(actualProperties, expectedProps, eventName); + assert(event, `${eventName} event not found`); assert.deepStrictEqual( - event.properties, - expectedProperties, + actualProperties, + expectedProps, `${eventName} event properties do not match`, ); } +function compareSecurityAlertResponse( + actualProperties: Record, + expectedProperties: Record, + eventName: string, +) { + if ( + expectedProperties.security_alert_response && + (expectedProperties.security_alert_response === 'loading' || + expectedProperties.security_alert_response === 'Benign') + ) { + if ( + actualProperties.security_alert_response !== 'loading' && + actualProperties.security_alert_response !== 'Benign' + ) { + assert.fail( + `${eventName} event properties do not match: security_alert_response is ${actualProperties.security_alert_response}`, + ); + } + // Remove the property from both objects to avoid comparison + delete actualProperties.security_alert_response; + delete expectedProperties.security_alert_response; + } +} + export async function clickHeaderInfoBtn(driver: Driver) { 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..889122eb22b8 100644 --- a/test/e2e/tests/confirmations/signatures/siwe.spec.ts +++ b/test/e2e/tests/confirmations/signatures/siwe.spec.ts @@ -11,6 +11,10 @@ import { withRedesignConfirmationFixtures, } from '../helpers'; import { TestSuiteArguments } from '../transactions/shared'; +import { + BlockaidReason, + BlockaidResultType, +} from '../../../../../shared/constants/security-provider'; import { assertAccountDetailsMetrics, assertHeaderInfoBalance, @@ -60,6 +64,8 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { 'redesigned_confirmation', 'sign_in_with_ethereum', ], + securityAlertReason: BlockaidReason.notApplicable, + securityAlertResponse: BlockaidResultType.NotApplicable, }); }, mockSignatureApproved, @@ -95,6 +101,8 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { 'sign_in_with_ethereum', ], location: 'confirmation', + securityAlertReason: BlockaidReason.notApplicable, + securityAlertResponse: BlockaidResultType.NotApplicable, }); }, mockSignatureRejected, @@ -103,6 +111,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/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/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/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/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index dfe77f758fcb..3b003b044b5a 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -46,6 +46,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 +59,7 @@ const removedBackgroundFields = [ 'AppStateController.currentPopupId', 'AppStateController.timeoutMinutes', 'AppStateController.lastInteractedConfirmationInfo', + 'BridgeController.bridgeState.quoteRequest.walletAddress', 'PPOMController.chainStatus.0x539.lastVisited', 'PPOMController.versionInfo', // This property is timing-dependent @@ -862,6 +865,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 +885,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/signature-approved.spec.js b/test/e2e/tests/metrics/signature-approved.spec.js index c6990820af8b..2ea84d281b50 100644 --- a/test/e2e/tests/metrics/signature-approved.spec.js +++ b/test/e2e/tests/metrics/signature-approved.spec.js @@ -1,4 +1,5 @@ const { strict: assert } = require('assert'); + const { defaultGanacheOptions, switchToNotificationWindow, @@ -47,6 +48,16 @@ async function mockSegment(mockServer) { ]; } +const expectedEventPropertiesBase = { + account_type: 'MetaMask', + category: 'inpage_provider', + locale: 'en', + chain_id: '0x539', + environment_type: 'background', + security_alert_reason: 'CheckingChain', + security_alert_response: 'loading', +}; + describe('Signature Approved Event @no-mmi', function () { it('Successfully tracked for signTypedData_v4', async function () { await withFixtures( @@ -76,31 +87,21 @@ describe('Signature Approved Event @no-mmi', function () { const events = await getEventPayloads(driver, mockedEndpoints); assert.deepStrictEqual(events[0].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'eth_signTypedData_v4', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', eip712_primary_type: 'Mail', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', }); assert.deepStrictEqual(events[1].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'eth_signTypedData_v4', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', eip712_primary_type: 'Mail', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', + security_alert_response: 'Benign', }); }, ); }); + it('Successfully tracked for signTypedData_v3', async function () { await withFixtures( { @@ -127,29 +128,21 @@ describe('Signature Approved Event @no-mmi', function () { await validateContractDetails(driver); await clickSignOnSignatureConfirmation({ driver }); const events = await getEventPayloads(driver, mockedEndpoints); + assert.deepStrictEqual(events[0].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'eth_signTypedData_v3', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', }); + assert.deepStrictEqual(events[1].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'eth_signTypedData_v3', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', + security_alert_response: 'Benign', }); }, ); }); + it('Successfully tracked for signTypedData', async function () { await withFixtures( { @@ -175,29 +168,21 @@ describe('Signature Approved Event @no-mmi', function () { await switchToNotificationWindow(driver); await clickSignOnSignatureConfirmation({ driver }); const events = await getEventPayloads(driver, mockedEndpoints); + assert.deepStrictEqual(events[0].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'eth_signTypedData', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', }); + assert.deepStrictEqual(events[1].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'eth_signTypedData', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', + security_alert_response: 'Benign', }); }, ); }); + it('Successfully tracked for personalSign', async function () { await withFixtures( { @@ -223,25 +208,16 @@ describe('Signature Approved Event @no-mmi', function () { await switchToNotificationWindow(driver); await clickSignOnSignatureConfirmation({ driver }); const events = await getEventPayloads(driver, mockedEndpoints); + assert.deepStrictEqual(events[0].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'personal_sign', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', }); + assert.deepStrictEqual(events[1].properties, { - account_type: 'MetaMask', + ...expectedEventPropertiesBase, signature_type: 'personal_sign', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', + security_alert_response: 'Benign', }); }, ); 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..6d77cd3ae351 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" }, 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..e577bb71a6be 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 @@ -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, @@ -187,7 +197,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 +213,6 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", - "submitHistory": "object", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", @@ -258,12 +267,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/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/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..1494d8b2ceb3 --- /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, + defaultGanacheOptions, + completeImportSRPOnboardingFlow, +} 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 { 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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..eb0c2c7b65e8 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts @@ -0,0 +1,118 @@ +import { Mockttp } from 'mockttp'; +import { + withFixtures, + defaultGanacheOptions, + completeImportSRPOnboardingFlow, + completeCreateNewWalletOnboardingFlow, +} 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 { 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + + // Create a new wallet + await completeCreateNewWalletOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + // 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + + // Onboard with import flow using SRP from new account created above + await completeImportSRPOnboardingFlow( + driver, + walletSrp, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + // 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..5cf0bb3c4d19 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts @@ -0,0 +1,168 @@ +import { Mockttp } from 'mockttp'; +import { + withFixtures, + defaultGanacheOptions, + completeImportSRPOnboardingFlow, + importSRPOnboardingFlow, + onboardingCompleteWalletCreationWithOptOut, + completeCreateNewWalletOnboardingFlowWithOptOut, +} 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 { accountsSyncMockResponse } from './mockData'; +import { getSRP, IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; + +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(), + ganacheOptions: defaultGanacheOptions, + 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 driver.navigate(); + await importSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + await onboardingCompleteWalletCreationWithOptOut(driver, { + isNewWallet: false, + basicFunctionality: false, + profileSync: true, + assets: false, + }); + + 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(), + ganacheOptions: defaultGanacheOptions, + 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 driver.navigate(); + await completeCreateNewWalletOnboardingFlowWithOptOut( + driver, + NOTIFICATIONS_TEAM_PASSWORD, + { + isNewWallet: true, + basicFunctionality: false, + profileSync: true, + assets: false, + }, + ); + + 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(), + ganacheOptions: defaultGanacheOptions, + 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 driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + walletSrp, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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..d6c0dc373f69 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts @@ -0,0 +1,212 @@ +import { Mockttp } from 'mockttp'; +import { + withFixtures, + defaultGanacheOptions, + completeImportSRPOnboardingFlow, +} 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 { 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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..418962b370de --- /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, + defaultGanacheOptions, + completeImportSRPOnboardingFlow, +} 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 { 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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..4ec904256525 --- /dev/null +++ b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts @@ -0,0 +1,69 @@ +import { Mockttp } from 'mockttp'; +import { + withFixtures, + defaultGanacheOptions, + completeImportSRPOnboardingFlow, +} 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 { 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(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: (server: Mockttp) => { + userStorageMockttpController.setupPath('accounts', server, { + getResponse: accountsSyncMockResponse, + }); + return mockNotificationServices( + server, + userStorageMockttpController, + ); + }, + }, + async ({ driver }) => { + await driver.navigate(); + await completeImportSRPOnboardingFlow( + driver, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + 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..aa04ca151f86 --- /dev/null +++ b/test/e2e/tests/onboarding/onboarding.spec.ts @@ -0,0 +1,271 @@ +import { + convertToHexValue, + 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); + + 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..67fb82f8fa55 100644 --- a/test/e2e/tests/phishing-controller/phishing-detection.spec.js +++ b/test/e2e/tests/phishing-controller/phishing-detection.spec.js @@ -59,7 +59,7 @@ describe('Phishing Detection', function () { await openDapp(driver); await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); await driver.clickElement({ - text: 'continue to the site.', + text: 'Proceed anyway', }); await driver.wait(until.titleIs(WINDOW_TITLES.TestDApp), 10000); }, @@ -104,7 +104,7 @@ describe('Phishing Detection', function () { await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); await driver.clickElement({ - text: 'continue to the site.', + text: 'Proceed anyway', }); await driver.wait(until.titleIs(WINDOW_TITLES.TestDApp), 10000); @@ -170,7 +170,7 @@ describe('Phishing Detection', function () { }); await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); 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 +253,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 +290,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..a945154f4bd3 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -26,8 +26,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, 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..b18d713d9474 --- /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/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/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/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 2d9e50002a18..7949e19cfa51 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -783,7 +783,8 @@ "showTestNetworks": true, "smartTransactionsOptInStatus": false, "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..e47d1379b2eb 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -227,7 +227,8 @@ "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/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..545d548fa3b7 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..55821f7080f5 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/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap index 179a3821cad4..f98b3a231970 100644 --- a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap +++ b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap @@ -11,6 +11,7 @@ exports[`CancelTransactionGasFee Component should render 1`] = ` > <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..cfa08b3eee01 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 @@ -706,15 +706,12 @@ exports[`NameDetails renders with recognized name 1`] = `
-
- -
+ />

diff --git a/ui/components/app/name/name-details/name-details.test.tsx b/ui/components/app/name/name-details/name-details.test.tsx index 9e93384adcd6..0f0df9f5b5f6 100644 --- a/ui/components/app/name/name-details/name-details.test.tsx +++ b/ui/components/app/name/name-details/name-details.test.tsx @@ -11,8 +11,8 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; -import { mockNetworkState } from '../../../../../test/stub/networks'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import NameDetails from './name-details'; jest.mock('../../../../store/actions', () => ({ @@ -37,11 +37,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 +85,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', + }, + }, }, }, }, @@ -157,6 +161,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -170,6 +175,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -183,6 +189,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -196,6 +203,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -209,6 +217,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -229,6 +238,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -251,6 +261,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -273,6 +284,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -295,6 +307,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -317,6 +330,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -336,6 +350,7 @@ describe('NameDetails', () => { undefined} />, store, @@ -373,6 +388,7 @@ describe('NameDetails', () => { undefined} /> , @@ -399,6 +415,7 @@ describe('NameDetails', () => { undefined} /> , @@ -426,6 +443,7 @@ describe('NameDetails', () => { undefined} /> , @@ -454,6 +472,7 @@ describe('NameDetails', () => { undefined} /> , 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..7dd3749a6e5f 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`] = `
= ({ [caipIdentifier], ); - // 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 displayName = useDisplayName(parsed); + + const value = + displayName ?? + shortenAddress( + parsed.chain.namespace === 'eip155' + ? toChecksumHexAddress(parsed.address) + : parsed.address, + ); return ( - + - {shortenedAddress} + {value} ); }; 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/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/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/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/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" > 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 ( ( 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..6be67515e6e4 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, }, diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 8cd56928fc66..568d62e7a2d4 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -8,7 +8,7 @@ import { getIsBridgeEnabled, getSwapsDefaultToken, SwapsEthToken, -} from '../../selectors'; +} from '../../selectors/selectors'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { BridgeControllerState, @@ -110,7 +110,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); diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 05cc6d46cb27..9627608eb709 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'; @@ -50,6 +50,7 @@ const initialState = { smartTransactionsOptInStatus: false, 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/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/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/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/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/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..5c36b0a97ed2 100644 --- a/ui/hooks/useDisplayName.test.ts +++ b/ui/hooks/useDisplayName.test.ts @@ -1,218 +1,533 @@ -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 { cloneDeep } from 'lodash'; +import { Hex } from '@metamask/utils'; +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import mockState from '../../test/data/mock-state.json'; +import { + EXPERIENCES_TYPE, + FIRST_PARTY_CONTRACT_NAMES, +} from '../../shared/constants/first-party-contracts'; import { useDisplayName } from './useDisplayName'; -import { useNames } from './useName'; -import { useFirstPartyContractNames } from './useFirstPartyContractName'; import { useNftCollectionsMetadata } from './useNftCollectionsMetadata'; +import { useNames } from './useName'; -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('../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, -}; +jest.mock('./useName'); +jest.mock('./useNftCollectionsMetadata'); + +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 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); - 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 }, + }, }); - }); + } + + function mockFirstPartyContractName( + value: string, + variation: string, + name: string, + ) { + FIRST_PARTY_CONTRACT_NAMES[name as EXPERIENCES_TYPE] = { + [variation as Hex]: value as Hex, + }; + } - it('prioritizes a petname over all else', () => { - useNamesMock.mockReturnValue([PETNAME_FOUND_RETURN_VALUE]); - useFirstPartyContractNamesMock.mockReturnValue([ - FIRST_PARTY_CONTRACT_NAME_MOCK, - ]); - getRemoteTokensMock.mockReturnValue([ + 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 a watched NFT name if no other name is found', () => { - getNftContractsByAddressOnCurrentChainMock.mockReturnValue( - WATCHED_NFT_FOUND_RETURN_VALUE, - ); + 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, + }); + }); + }); - const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK)); + 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, + }); + }); - expect(result.current).toEqual({ - name: WATCHED_NFT_NAME_MOCK, - hasPetname: false, + 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, + }); }); }); - it('returns nft collection name from metadata if no other name is found', () => { - const IMAGE_MOCK = 'url'; + 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, + }); + }); - useNftCollectionsMetadataMock.mockReturnValue({ - [VALUE_MOCK.toLowerCase()]: { - name: CONTRACT_NAME_MOCK, - image: IMAGE_MOCK, - isSpam: false, - }, + 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, + }); + }); + }); + + 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, + }); }); - const { result } = renderHook(() => - useDisplayName(VALUE_MOCK, TYPE_MOCK, 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, + }); + }); - expect(result.current).toEqual({ - name: CONTRACT_NAME_MOCK, - hasPetname: false, - contractDisplayName: undefined, - image: IMAGE_MOCK, + 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, + }); }); }); - 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..7b7429c7a0d4 100644 --- a/ui/hooks/useDisplayName.ts +++ b/ui/hooks/useDisplayName.ts @@ -1,16 +1,20 @@ -import { useMemo } from 'react'; import { NameType } from '@metamask/name-controller'; import { useSelector } from 'react-redux'; -import { getRemoteTokens } from '../selectors'; -import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft'; +import { Hex } from '@metamask/utils'; +import { selectERC20TokensByChain } from '../selectors'; +import { getNftContractsByAddressByChain } from '../selectors/nft'; +import { + EXPERIENCES_TYPE, + FIRST_PARTY_CONTRACT_NAMES, +} from '../../shared/constants/first-party-contracts'; 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 +27,145 @@ export type UseDisplayNameResponse = { export function useDisplayNames( requests: UseDisplayNameRequest[], ): UseDisplayNameResponse[] { - const nameRequests = useMemo( - () => requests.map(({ value, type }) => ({ value, type })), - [requests], - ); - - const nameEntries = useNames(nameRequests); - const firstPartyContractNames = useFirstPartyContractNames(nameRequests); - const nftCollections = useNftCollectionsMetadata(nameRequests); - const values = requests.map(({ value }) => value); - - const contractInfo = useSelector((state) => - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (getRemoteTokens as any)(state, values), - ); + const nameEntries = useNames(requests); + const firstPartyContractNames = useFirstPartyContractNames(requests); + const erc20Tokens = useERC20Tokens(requests); + const watchedNFTNames = useWatchedNFTNames(requests); + const nfts = useNFTs(requests); - 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 name = nameEntry?.name || firstPartyContractName || - nftCollectionName || - contractDisplayName || + nft?.name || + erc20Token?.name || watchedNftName || 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 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/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..f746c4bb6267 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', () => ({ @@ -19,13 +19,14 @@ jest.mock('../selectors', () => ({ 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 +36,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 +43,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 +62,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 +72,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 +86,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 +96,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 +110,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 +120,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 +145,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 +159,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 +175,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 +186,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 +200,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..e1e2b6745ad1 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, @@ -42,7 +41,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 +48,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 +64,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 +78,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 +100,8 @@ describe('useNftCollectionsMetadata', () => { renderHook(() => useNftCollectionsMetadata([ { - value: '0xERC20Address', + chainId: CHAIN_ID_MOCK, + contractAddress: '0xERC20Address', }, ]), ); @@ -114,7 +116,8 @@ describe('useNftCollectionsMetadata', () => { renderHook(() => useNftCollectionsMetadata([ { - value: '0xERC20Address', + chainId: CHAIN_ID_MOCK, + contractAddress: '0xERC20Address', }, ]), ); @@ -126,10 +129,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/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index 95828e3e250e..79400367de13 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.

- 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.

{ + 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/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap index b406cafe0941..4284c1893d7c 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 @@ -107,6 +107,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] = > $0.00 @@ -191,6 +192,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] = > $0.00 @@ -316,6 +318,7 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` > $0.00 @@ -444,6 +447,7 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` > $0.00 diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 2fdb11289c5b..b0907f83dab7 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -1,13 +1,15 @@ -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import classnames from 'classnames'; +import { debounce } from 'lodash'; import { setFromChain, setFromToken, setFromTokenInputValue, setToChain, + setToChainId, setToToken, - switchToAndFromTokens, + updateQuoteRequestParams, } from '../../../ducks/bridge/actions'; import { getFromAmount, @@ -28,11 +30,14 @@ import { ButtonIcon, IconName, } from '../../../components/component-library'; +import { BlockSize } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { TokenBucketPriority } from '../../../../shared/constants/swaps'; import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering'; import { setActiveNetwork } from '../../../store/actions'; -import { BlockSize } from '../../../helpers/constants/design-system'; +import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; +import { QuoteRequest } from '../types'; +import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; import { BridgeInputGroup } from './bridge-input-group'; const PrepareBridgePage = () => { @@ -71,6 +76,36 @@ const PrepareBridgePage = () => { const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); + const quoteParams = useMemo( + () => ({ + srcTokenAddress: fromToken?.address, + destTokenAddress: toToken?.address || undefined, + srcTokenAmount: + fromAmount && fromAmount !== '' && fromToken?.decimals + ? calcTokenValue(fromAmount, fromToken.decimals).toString() + : undefined, + srcChainId: fromChain?.chainId + ? Number(hexToDecimal(fromChain.chainId)) + : undefined, + destChainId: toChain?.chainId + ? Number(hexToDecimal(toChain.chainId)) + : undefined, + }), + [fromToken, toToken, fromChain?.chainId, toChain?.chainId, fromAmount], + ); + + const debouncedUpdateQuoteRequestInController = useCallback( + debounce( + (p: Partial) => dispatch(updateQuoteRequestParams(p)), + 300, + ), + [], + ); + + useEffect(() => { + debouncedUpdateQuoteRequestInController(quoteParams); + }, Object.values(quoteParams)); + return (
@@ -81,7 +116,10 @@ const PrepareBridgePage = () => { onAmountChange={(e) => { dispatch(setFromTokenInputValue(e)); }} - onAssetChange={(token) => dispatch(setFromToken(token))} + onAssetChange={(token) => { + dispatch(setFromToken(token)); + dispatch(setFromTokenInputValue(null)); + }} networkProps={{ network: fromChain, networks: fromChains, @@ -94,6 +132,8 @@ const PrepareBridgePage = () => { ), ); dispatch(setFromChain(networkConfig.chainId)); + dispatch(setFromToken(null)); + dispatch(setFromTokenInputValue(null)); }, }} customTokenListGenerator={ @@ -121,12 +161,18 @@ const PrepareBridgePage = () => { onClick={() => { setRotateSwitchTokens(!rotateSwitchTokens); const toChainClientId = - toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints - ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex] + toChain?.defaultRpcEndpointIndex !== undefined && + toChain?.rpcEndpoints + ? toChain.rpcEndpoints[toChain.defaultRpcEndpointIndex] .networkClientId : undefined; toChainClientId && dispatch(setActiveNetwork(toChainClientId)); - dispatch(switchToAndFromTokens({ fromChain })); + toChain && dispatch(setFromChain(toChain.chainId)); + dispatch(setFromToken(toToken)); + dispatch(setFromTokenInputValue(null)); + fromChain?.chainId && dispatch(setToChain(fromChain.chainId)); + fromChain?.chainId && dispatch(setToChainId(fromChain.chainId)); + dispatch(setToToken(fromToken)); }} /> @@ -140,6 +186,7 @@ const PrepareBridgePage = () => { network: toChain, networks: toChains, onNetworkChange: (networkConfig) => { + dispatch(setToChainId(networkConfig.chainId)); dispatch(setToChain(networkConfig.chainId)); }, }} diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts new file mode 100644 index 000000000000..5d001e7ef7fc --- /dev/null +++ b/ui/pages/bridge/types.ts @@ -0,0 +1,119 @@ +// Types copied from Metabridge API +export enum BridgeFlag { + EXTENSION_CONFIG = 'extension-config', + EXTENSION_SUPPORT = 'extension-support', + NETWORK_SRC_ALLOWLIST = 'src-network-allowlist', + NETWORK_DEST_ALLOWLIST = 'dest-network-allowlist', +} + +export type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + }; + [BridgeFlag.EXTENSION_SUPPORT]: boolean; + [BridgeFlag.NETWORK_SRC_ALLOWLIST]: number[]; + [BridgeFlag.NETWORK_DEST_ALLOWLIST]: number[]; +}; + +export type BridgeAsset = { + chainId: ChainId; + address: string; + symbol: string; + name: string; + decimals: number; + icon?: string; +}; + +export type QuoteRequest = { + walletAddress: string; + destWalletAddress?: string; + srcChainId: ChainId; + destChainId: ChainId; + srcTokenAddress: string; + destTokenAddress: string; + srcTokenAmount: string; + slippage: number; + aggIds?: string[]; + bridgeIds?: string[]; + insufficientBal?: boolean; + resetApproval?: boolean; + refuel?: boolean; +}; + +type Protocol = { + name: string; + displayName?: string; + icon?: string; +}; + +enum ActionTypes { + BRIDGE = 'bridge', + SWAP = 'swap', + REFUEL = 'refuel', +} + +type Step = { + action: ActionTypes; + srcChainId: ChainId; + destChainId?: ChainId; + srcAsset: BridgeAsset; + destAsset: BridgeAsset; + srcAmount: string; + destAmount: string; + protocol: Protocol; +}; + +type RefuelData = Step; + +export type Quote = { + requestId: string; + srcChainId: ChainId; + srcAsset: BridgeAsset; + srcTokenAmount: string; + destChainId: ChainId; + destAsset: BridgeAsset; + destTokenAmount: string; + feeData: Record & + Partial>; + bridgeId: string; + bridges: string[]; + steps: Step[]; + refuel?: RefuelData; +}; + +export type QuoteResponse = { + quote: Quote; + approval: TxData | null; + trade: TxData; + estimatedProcessingTimeInSeconds: number; +}; + +enum ChainId { + ETH = 1, + OPTIMISM = 10, + BSC = 56, + POLYGON = 137, + ZKSYNC = 324, + BASE = 8453, + ARBITRUM = 42161, + AVALANCHE = 43114, + LINEA = 59144, +} + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', +} +export type FeeData = { + amount: string; + asset: BridgeAsset; +}; +export type TxData = { + chainId: ChainId; + to: string; + from: string; + value: string; + data: string; + gasLimit: number | null; +}; diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts new file mode 100644 index 000000000000..0b83205580b4 --- /dev/null +++ b/ui/pages/bridge/utils/quote.ts @@ -0,0 +1,33 @@ +import { QuoteRequest } from '../types'; + +export const isValidQuoteRequest = ( + partialRequest: Partial, + requireAmount = true, +): partialRequest is QuoteRequest => { + const STRING_FIELDS = ['srcTokenAddress', 'destTokenAddress']; + if (requireAmount) { + STRING_FIELDS.push('srcTokenAmount'); + } + const NUMBER_FIELDS = ['srcChainId', 'destChainId', 'slippage']; + + return ( + STRING_FIELDS.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'string' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + partialRequest[field as keyof typeof partialRequest] !== '' && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + NUMBER_FIELDS.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'number' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + !isNaN(Number(partialRequest[field as keyof typeof partialRequest])) && + partialRequest[field as keyof typeof partialRequest] !== null, + ) + ); +}; diff --git a/ui/pages/bridge/utils/validators.ts b/ui/pages/bridge/utils/validators.ts new file mode 100644 index 000000000000..01c716522968 --- /dev/null +++ b/ui/pages/bridge/utils/validators.ts @@ -0,0 +1,105 @@ +import { isStrictHexString } from '@metamask/utils'; +import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; +import { + truthyDigitString, + validateData, +} from '../../../../shared/lib/swaps-utils'; +import { BridgeFlag, FeatureFlagResponse } from '../types'; + +type Validator = { + property: keyof ExpectedResponse | string; + type: string; + validator?: (value: unknown) => boolean; +}; + +export const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, +): data is ExpectedResponse => { + return validateData(validators, data, urlUsed); +}; + +export const isValidNumber = (v: unknown): v is number => typeof v === 'number'; +const isValidObject = (v: unknown): v is object => + typeof v === 'object' && v !== null; +const isValidString = (v: unknown): v is string => + typeof v === 'string' && v.length > 0; +const isValidHexAddress = (v: unknown) => + isValidString(v) && isValidHexAddress_(v, { allowNonPrefixed: false }); + +export const FEATURE_FLAG_VALIDATORS = [ + { + property: BridgeFlag.EXTENSION_CONFIG, + type: 'object', + validator: ( + v: unknown, + ): v is Pick => + isValidObject(v) && + 'refreshRate' in v && + isValidNumber(v.refreshRate) && + 'maxRefreshCount' in v && + isValidNumber(v.maxRefreshCount), + }, + { property: BridgeFlag.EXTENSION_SUPPORT, type: 'boolean' }, + { + property: BridgeFlag.NETWORK_SRC_ALLOWLIST, + type: 'object', + validator: (v: unknown): v is number[] => + isValidObject(v) && Object.values(v).every(isValidNumber), + }, + { + property: BridgeFlag.NETWORK_DEST_ALLOWLIST, + type: 'object', + validator: (v: unknown): v is number[] => + isValidObject(v) && Object.values(v).every(isValidNumber), + }, +]; + +export const TOKEN_VALIDATORS = [ + { property: 'decimals', type: 'number' }, + { property: 'address', type: 'string', validator: isValidHexAddress }, + { + property: 'symbol', + type: 'string', + validator: (v: unknown) => isValidString(v) && v.length <= 12, + }, +]; + +export const QUOTE_RESPONSE_VALIDATORS = [ + { property: 'quote', type: 'object', validator: isValidObject }, + { property: 'estimatedProcessingTimeInSeconds', type: 'number' }, + { + property: 'approval', + type: 'object|undefined', + validator: (v: unknown) => v === undefined || isValidObject(v), + }, + { property: 'trade', type: 'object', validator: isValidObject }, +]; + +export const QUOTE_VALIDATORS = [ + { property: 'requestId', type: 'string' }, + { property: 'srcTokenAmount', type: 'string' }, + { property: 'destTokenAmount', type: 'string' }, + { property: 'bridgeId', type: 'string' }, + { property: 'bridges', type: 'object', validator: isValidObject }, + { property: 'srcChainId', type: 'number' }, + { property: 'destChainId', type: 'number' }, + { property: 'srcAsset', type: 'object', validator: isValidObject }, + { property: 'destAsset', type: 'object', validator: isValidObject }, + { property: 'feeData', type: 'object', validator: isValidObject }, +]; + +export const FEE_DATA_VALIDATORS = [ + { property: 'amount', type: 'string', validator: truthyDigitString }, + { property: 'asset', type: 'object', validator: isValidObject }, +]; + +export const TX_DATA_VALIDATORS = [ + { property: 'chainId', type: 'number' }, + { property: 'value', type: 'string', validator: isStrictHexString }, + { property: 'gasLimit', type: 'number' }, + { property: 'to', type: 'string', validator: isValidHexAddress }, + { property: 'from', type: 'string', validator: isValidHexAddress }, + { property: 'data', type: 'string', validator: isStrictHexString }, +]; diff --git a/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap b/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap index cb4715881e27..d5e060e31d72 100644 --- a/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap +++ b/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConfirmDecryptMessage Component should match snapshot when preference is ETH currency 1`] = ` +exports[`ConfirmDecryptMessage Component matches snapshot 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 +
+
-