diff --git a/.circleci/config.yml b/.circleci/config.yml index 3836d5e4048e..aa13dea93b75 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -444,7 +444,7 @@ jobs: - gh/install - run: name: Install dependencies - command: .circleci/scripts/install-dependencies.sh + command: yarn --immutable - save_cache: key: dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }} paths: diff --git a/.circleci/scripts/install-dependencies.sh b/.circleci/scripts/install-dependencies.sh deleted file mode 100755 index 35b7a690fa0c..000000000000 --- a/.circleci/scripts/install-dependencies.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -IS_NON_FORK_DRAFT='false' - -if [[ -n $CIRCLE_PULL_REQUEST ]] && gh auth status -then - PR_NUMBER="${CIRCLE_PULL_REQUEST##*/}" - if [ -n "$PR_NUMBER" ] - then - IS_NON_FORK_DRAFT="$(gh pr view --json isDraft --jq '.isDraft' "$PR_NUMBER")" - fi -fi - -# Build query to see whether there are any "preview-like" packages in the manifest -# A "preview-like" package is a `@metamask`-scoped package with a prerelease version that has no period. -QUERY='.dependencies + .devDependencies' # Get list of all dependencies -QUERY+=' | with_entries( select(.key | startswith("@metamask") ) )' # filter to @metamask-scoped packages -QUERY+=' | to_entries[].value' # Get version ranges -QUERY+=' | select(test("^\\d+\\.\\d+\\.\\d+-[^.]+$"))' # Get pinned versions where the prerelease part has no "." - -# Use `-e` flag so that exit code indicates whether any matches were found -if jq -e "${QUERY}" < ./package.json -then - echo "Preview builds detected" - HAS_PREVIEW_BUILDS='true' -else - echo "No preview builds detected" - HAS_PREVIEW_BUILDS='false' -fi - -if [[ $IS_NON_FORK_DRAFT == 'true' && $HAS_PREVIEW_BUILDS == 'true' ]] -then - # Use GitHub registry on draft PRs, allowing the use of preview builds - echo "Installing with preview builds" - METAMASK_NPM_REGISTRY=https://npm.pkg.github.com yarn --immutable -else - echo "Installing without preview builds" - yarn --immutable -fi diff --git a/.depcheckrc.yml b/.depcheckrc.yml index d0d6eac5b5bc..50b79a78ec30 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -81,6 +81,8 @@ ignores: # trezor - 'ts-mixer' - '@testing-library/dom' + - 'mini-css-extract-plugin' + - 'webpack-cli' # files depcheck should not parse ignorePatterns: diff --git a/.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/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch b/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch similarity index 100% rename from .yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch rename to .yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch diff --git a/.yarnrc.yml b/.yarnrc.yml index cc0c959e2722..8e12d8037c6a 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -119,14 +119,6 @@ npmAuditIgnoreAdvisories: - 'react-beautiful-dnd (deprecation)' # New package name format for new versions: @ethereumjs/wallet. - 'ethereumjs-wallet (deprecation)' -npmRegistries: - 'https://npm.pkg.github.com': - npmAlwaysAuth: true - npmAuthToken: '${GITHUB_PACKAGE_READ_TOKEN-}' - -npmScopes: - metamask: - npmRegistryServer: '${METAMASK_NPM_REGISTRY:-https://registry.yarnpkg.com}' plugins: - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b33f07fb3d5..af63a4ed61d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.6.0] +### Added +- Added the APE network icon ([#27841](https://github.com/MetaMask/metamask-extension/pull/27841)) +- Added token sorting and improved token importing on the Asset List page ([#27184](https://github.com/MetaMask/metamask-extension/pull/27184)) +- Added an aggregated balance feature and updated settings to toggle between fiat and native token balances ([#27097](https://github.com/MetaMask/metamask-extension/pull/27097)) +- Added a network picker to the AssetPicker for easier cross-chain swaps ([#26559](https://github.com/MetaMask/metamask-extension/pull/26559)) +- Added new header and conditional simulations for dapp-initiated token transfer confirmations ([#27875](https://github.com/MetaMask/metamask-extension/pull/27875)) +- Added simulation section to NFT permit confirmations ([#27825](https://github.com/MetaMask/metamask-extension/pull/27825)) +- Added transaction flow and details sections for wallet-initiated ERC20 token transfer confirmations ([#27654](https://github.com/MetaMask/metamask-extension/pull/27654)) +- Added support for typed sign requests for NFT permits ([#27796](https://github.com/MetaMask/metamask-extension/pull/27796)) +- Added support for gas fee flows in standard swaps on EIP-1559 networks ([#27612](https://github.com/MetaMask/metamask-extension/pull/27612)) +- Added a Token Send Heading component ([#27562](https://github.com/MetaMask/metamask-extension/pull/27562)) +- Added support for Etherscan API keys and improved transaction history logging ([#27611](https://github.com/MetaMask/metamask-extension/pull/27611)) +- Added a custom header for wallet-initiated ERC20 token transfer confirmations ([#27391](https://github.com/MetaMask/metamask-extension/pull/27391)) +- Added redesigned screens for setApprovalForAll and revoke setApprovalForAll for users who opt into experimental transaction screens ([#27401](https://github.com/MetaMask/metamask-extension/pull/27401)) +- Added new screens for approve, increaseAllowance, and revoke approval for users who enable experimental transaction screens ([#26985](https://github.com/MetaMask/metamask-extension/pull/26985)) +- Added support for revoking ERC20 allowances ([#26906](https://github.com/MetaMask/metamask-extension/pull/26906)) +- Added a "Delete MetaMetrics Data" button to the Security & Privacy tab, allowing users to delete their MetaMetrics data ([#24571](https://github.com/MetaMask/metamask-extension/pull/24571)) +- Added a new Default Settings view and updated Congratulations views in the onboarding process ([#24562](https://github.com/MetaMask/metamask-extension/pull/24562)) +- Added a delay for Linea swap approvals to increase success rate and updated token symbol retrieval on the awaiting swap page ([#27810](https://github.com/MetaMask/metamask-extension/pull/27810)) +- Enabled smart transactions by default for new users and updated selectors to handle user preferences and metrics separately ([#27885](https://github.com/MetaMask/metamask-extension/pull/27885)) +- Added animations and cosmetic changes to the smart transaction status page ([#27650](https://github.com/MetaMask/metamask-extension/pull/27650)) +- Enabled gas-included swaps for users with insufficient ETH when smart transactions are enabled ([#27427](https://github.com/MetaMask/metamask-extension/pull/27427)) +- Added padding to center-align text on the permissions page when no site or snap is connected ([#27660](https://github.com/MetaMask/metamask-extension/pull/27660)) +- Released Chain Permissions by removing feature flags ([#27561](https://github.com/MetaMask/metamask-extension/pull/27561)) +- Added support for power users survey with toast notifications ([#27361](https://github.com/MetaMask/metamask-extension/pull/27361)) +- Added editing flow for switching networks via dapp ([#26635](https://github.com/MetaMask/metamask-extension/pull/26635)) +- [FLASK] Added the ability to send Bitcoin from Bitcoin accounts ([#27964](https://github.com/MetaMask/metamask-extension/pull/27964)) + +### Changed +- Bumped snap-keyring to version 4.4.0 to sanitize redirect URLs passed by a Snap ([#27864](https://github.com/MetaMask/metamask-extension/pull/27864)) +- Updated the insufficient funds alert to replace "transaction fees" with "network fees." ([#27762](https://github.com/MetaMask/metamask-extension/pull/27762)) +- Updated the SIWE signature page to display the parsed URI instead of the domain ([#27754](https://github.com/MetaMask/metamask-extension/pull/27754)) +- Limited the number of decimals on the spending cap modal to match the token's supported decimals ([#27672](https://github.com/MetaMask/metamask-extension/pull/27672)) +- Updated petnames component to prefer displaying token symbols over token names for brevity ([#27693](https://github.com/MetaMask/metamask-extension/pull/27693)) +- Updated banner alert to render multiple general alerts and fixed related UI issues ([#27339](https://github.com/MetaMask/metamask-extension/pull/27339)) +- Updated Trezor Connect to v9.4.0 and removed outdated workarounds ([#27112](https://github.com/MetaMask/metamask-extension/pull/27112)) +- Restored the ability to switch between pending confirmations when routed to a specific confirmation ([#27753](https://github.com/MetaMask/metamask-extension/pull/27753)) +- Updated edit modals with design improvements and a fixed update button ([#27623](https://github.com/MetaMask/metamask-extension/pull/27623)) +- Updated copy for the onboarding message and settings screens ([#27821](https://github.com/MetaMask/metamask-extension/pull/27821)) +- Updated copy and spacing in the Permissions Screen ([#27658](https://github.com/MetaMask/metamask-extension/pull/27658)) +- Removed phishing detection from the onboarding Security group ([#27819](https://github.com/MetaMask/metamask-extension/pull/27819)) +- Removed the "Alerts" section from Settings, keeping alert features enabled by default ([#27709](https://github.com/MetaMask/metamask-extension/pull/27709)) +- Updated the toast component and its copy ([#27656](https://github.com/MetaMask/metamask-extension/pull/27656)) +- Changed survey timeout from one week to one day ([#27603](https://github.com/MetaMask/metamask-extension/pull/27603)) +- Updated UI for the connect and review permissions pages ([#27478](https://github.com/MetaMask/metamask-extension/pull/27478)) + +### Fixed +- Fixed an error when starting a "Send ETH" flow from a dapp with a Bitcoin account selected ([#27566](https://github.com/MetaMask/metamask-extension/pull/27566)) +- Fixed currency display to show token balance when fiat conversion rate is unavailable ([#27893](https://github.com/MetaMask/metamask-extension/pull/27893)) +- Fixed the issue where the add token modal couldn't be dismissed in MMI ([#27855](https://github.com/MetaMask/metamask-extension/pull/27855)) +- Fixed an issue that caused the app to crash when switching networks ([#27604](https://github.com/MetaMask/metamask-extension/pull/27604)) +- Fixed navigation error between transactions when one transaction is of type "Approve All." ([#27985](https://github.com/MetaMask/metamask-extension/pull/27985)) +- Fixed nonce value updating issue when multiple transactions are created in parallel ([#27874](https://github.com/MetaMask/metamask-extension/pull/27874)) +- Fixed issue with nonce not resetting when switching networks ([#27789](https://github.com/MetaMask/metamask-extension/pull/27789)) +- Fixed design issues and spacing in the redesigned transactions, and corrected loader behavior for confirmations ([#27605](https://github.com/MetaMask/metamask-extension/pull/27605)) +- Fixed bugs related to max approval values and array value spending caps ([#27573](https://github.com/MetaMask/metamask-extension/pull/27573)) +- Reverted the color change for the "Speed" key by removing the variant causing the issue ([#27416](https://github.com/MetaMask/metamask-extension/pull/27416)) +- Improved token decimal handling by using verified contract details when available and added support for tokens with null decimals ([#27328](https://github.com/MetaMask/metamask-extension/pull/27328)) +- Improved the alert system and refined alerts for SIWE and contract interactions ([#27205](https://github.com/MetaMask/metamask-extension/pull/27205)) +- Fixed an issue where entering a backslash in the settings search would cause a crash ([#27432](https://github.com/MetaMask/metamask-extension/pull/27432)) +- Automatically expand the first insight on the confirmation page ([#27872](https://github.com/MetaMask/metamask-extension/pull/27872)) +- Removed HTML arrows from custom UI inputs of type number in Snaps ([#27953](https://github.com/MetaMask/metamask-extension/pull/27953)) +- Hid the options menu and info icon in the Snaps header for preinstalled Snaps ([#27937](https://github.com/MetaMask/metamask-extension/pull/27937)) +- Fixed sticky footer UI issue on Snaps Home Page in extended view ([#27799](https://github.com/MetaMask/metamask-extension/pull/27799)) +- Fixed issue with Snap name truncation in the Snap Authorship Header ([#27752](https://github.com/MetaMask/metamask-extension/pull/27752)) +- Fixed the color of the "more" button in the Copyable component ([#27600](https://github.com/MetaMask/metamask-extension/pull/27600)) +- Fixed alignment issue by applying flex to Snaps buttons only when containing images and icons ([#27564](https://github.com/MetaMask/metamask-extension/pull/27564)) +- Fixed issue with input focus being lost on re-render in Snaps interfaces ([#27429](https://github.com/MetaMask/metamask-extension/pull/27429)) +- Fixed issue where state updates with falsy values were ignored in Snaps interfaces ([#27488](https://github.com/MetaMask/metamask-extension/pull/27488)) +- Fixed text color for secondary buttons in Snaps footer on hover and corrected footer variant when only one action is provided ([#27335](https://github.com/MetaMask/metamask-extension/pull/27335)) +- Fixed an issue where hardware wallet users were taken to the "Processing..." screen before approving transactions during swaps ([#27117](https://github.com/MetaMask/metamask-extension/pull/27117)) + +## [12.5.1] +### Changed +- Improve accuracy of transaction simulation warnings in some scenarios ([#26845](https://github.com/MetaMask/metamask-extension/pull/26845)) + +### Fixed +- Fix bug that could cause token balances to appear as zero, and a balance error to be displayed, on the send screen ([#28136](https://github.com/MetaMask/metamask-extension/pull/28136)) + ## [12.5.0] ### Added - New UI and functionality for adding and managing networks ([#26433](https://github.com/MetaMask/metamask-extension/pull/26433)), ([#27085](https://github.com/MetaMask/metamask-extension/pull/27085)) - Instead of having different networks in the network list for the same chain but different RPC urls, there are now multiple selectable RPC urls per chain - For the UI, networks are now added, edited, and deleted directly in the network list. Networks are no longer edited via the settings page. - Users with multiple RPC endpoints per chain are shown a modal upon upgrade, allowing them to select a different endpoint as the default. - - The UI for wallet_addEthereumChain is changed, to message that users may be adding an additional endpoint to an existing network, rather than adding a new network. + - The UI for wallet_addEthereumChain is changed, to message that users may be adding an additional endpoint to an existing network, rather than adding a new network. - Added display of names and images for ERC721 NFTs to the simulations in transaction confirmations ([#25692](https://github.com/MetaMask/metamask-extension/pull/25692)) - Added a modal to edit the spending cap for ERC20 approve and increase allowance ([#26845](https://github.com/MetaMask/metamask-extension/pull/26845)) - Added a new modal to help users with zero balance buy, receive, or transfer tokens ([#26426](https://github.com/MetaMask/metamask-extension/pull/26426)) @@ -5223,7 +5303,9 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.5.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.6.0...HEAD +[12.6.0]: https://github.com/MetaMask/metamask-extension/compare/v12.5.1...v12.6.0 +[12.5.1]: https://github.com/MetaMask/metamask-extension/compare/v12.5.0...v12.5.1 [12.5.0]: https://github.com/MetaMask/metamask-extension/compare/v12.4.2...v12.5.0 [12.4.2]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...v12.4.2 [12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 8d1ed215aa92..9af24022bcb3 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Jetzt kaufen" }, - "buyToken": { - "message": "$1 kaufen", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 7ccc23b123bb..308099b1c2b1 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Αγοράστε Τώρα" }, - "buyToken": { - "message": "Αγορά $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1883,13 +1879,6 @@ "functionType": { "message": "Τύπος λειτουργίας" }, - "fundYourWallet": { - "message": "Χρηματοδοτήστε το πορτοφόλι σας" - }, - "fundYourWalletDescription": { - "message": "Ξεκινήστε προσθέτοντας περίπου $1 στο πορτοφόλι σας.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Τέλος συναλλαγής" }, @@ -1973,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": "Πηγαίνετε πίσω" }, @@ -4994,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": "Σφάλμα κατά την ανάκτηση αρχείων καταγραφής κατάστασης." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 8c02c3ced035..08675837b34d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -413,6 +413,9 @@ "alertMessageAddressMismatchWarning": { "message": "Attackers sometimes mimic sites by making small changes to the site address. Make sure you're interacting with the intended site before you continue." }, + "alertMessageChangeInSimulationResults": { + "message": "Estimated changes for this transaction have been updated. Review them closely before proceeding." + }, "alertMessageGasEstimateFailed": { "message": "We’re unable to provide an accurate fee and this estimate might be high. We suggest you to input a custom gas limit, but there’s a risk the transaction will still fail." }, @@ -452,6 +455,9 @@ "alertModalReviewAllAlerts": { "message": "Review all alerts" }, + "alertReasonChangeInSimulationResults": { + "message": "Results have changed" + }, "alertReasonGasEstimateFailed": { "message": "Inaccurate fee" }, @@ -491,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" @@ -865,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" @@ -878,10 +882,6 @@ "buyNow": { "message": "Buy Now" }, - "buyToken": { - "message": "Buy $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1167,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" }, @@ -1345,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." }, @@ -1504,6 +1512,9 @@ "dcent": { "message": "D'Cent" }, + "debitCreditPurchaseOptions": { + "message": "Debit or credit card purchase options" + }, "decimal": { "message": "Token decimal" }, @@ -1813,6 +1824,9 @@ "editSpendingCapError": { "message": "The spending cap can’t exceed $1 decimal digits. Remove decimal digits to continue." }, + "editSpendingCapSpecialCharError": { + "message": "Enter numbers only" + }, "enable": { "message": "Enable" }, @@ -2096,12 +2110,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" @@ -2192,20 +2202,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" }, @@ -4442,6 +4438,9 @@ "requestFromInfo": { "message": "This is the site asking for your signature." }, + "requestFromInfoSnap": { + "message": "This is the Snap asking for your signature." + }, "requestFromTransactionDescription": { "message": "This is the site asking for your confirmation." }, @@ -4464,6 +4463,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" }, @@ -4746,9 +4749,6 @@ "selectEnableDisplayMediaPrivacyPreference": { "message": "Turn on Display NFT Media" }, - "selectFundingMethod": { - "message": "Select a funding method" - }, "selectHdPath": { "message": "Select HD path" }, @@ -5405,14 +5405,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." }, @@ -5899,6 +5891,10 @@ "swapTokenVerifiedOn1SourceTitle": { "message": "Potentially inauthentic token" }, + "swapTokenVerifiedSources": { + "message": "Confirmed by $1 sources. Verify on $2.", + "description": "$1 the number of sources that have verified the token, $2 points the user to a block explorer as a place they can verify information about the token." + }, "swapTooManyDecimalsError": { "message": "$1 allows up to $2 decimals", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -6016,6 +6012,12 @@ "tips": { "message": "Tips" }, + "tipsForUsingAWallet": { + "message": "Tips for using a wallet" + }, + "tipsForUsingAWalletDescription": { + "message": "Adding tokens unlocks more ways to use web3." + }, "to": { "message": "To" }, @@ -6068,6 +6070,9 @@ "tokenList": { "message": "Token lists" }, + "tokenMarketplace": { + "message": "Token marketplace" + }, "tokenScamSecurityRisk": { "message": "token scams and security risks" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index a2578d1f4137..ada162b9a12b 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -802,10 +802,6 @@ "buyNow": { "message": "Comprar ahora" }, - "buyToken": { - "message": "Comprar $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1880,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" }, @@ -1970,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" }, @@ -4991,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." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index c3151d3ca53f..856638ba2b8a 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Achetez maintenant" }, - "buyToken": { - "message": "Acheter des $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Octets" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index c69c7117d4d0..45e64a972e17 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "अभी खरीदें" }, - "buyToken": { - "message": "$1 खरीदें", - "description": "$1 is the token symbol" - }, "bytes": { "message": "बाइट" }, @@ -1883,13 +1879,6 @@ "functionType": { "message": "फ़ंक्शन का प्रकार" }, - "fundYourWallet": { - "message": "अपने वॉलेट को फंड करें" - }, - "fundYourWalletDescription": { - "message": "अपने वॉलेट में कुछ $1 जोड़कर शुरुआत करें।", - "description": "$1 is the token symbol" - }, "gas": { "message": "गैस" }, @@ -1973,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": "वापस जाएं" }, @@ -4994,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": "स्टेट लॉग को पुनर्प्राप्त करने में गड़बड़ी।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 53e7fd923fda..6314d9ed3468 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Beli Sekarang" }, - "buyToken": { - "message": "Beli $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Byte" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index c26ec67990b2..61730b2bc325 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "今すぐ購入" }, - "buyToken": { - "message": "$1を購入", - "description": "$1 is the token symbol" - }, "bytes": { "message": "バイト" }, @@ -1883,13 +1879,6 @@ "functionType": { "message": "機能の種類" }, - "fundYourWallet": { - "message": "ウォレットへの入金" - }, - "fundYourWalletDescription": { - "message": "ウォレットに$1を追加して開始します。", - "description": "$1 is the token symbol" - }, "gas": { "message": "ガス" }, @@ -1973,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": "戻る" }, @@ -4994,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": "ステートログの取得中にエラーが発生しました。" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index ad4959a459b9..05c04fbd17a9 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "지금 구매" }, - "buyToken": { - "message": "$1 구매", - "description": "$1 is the token symbol" - }, "bytes": { "message": "바이트" }, @@ -1883,13 +1879,6 @@ "functionType": { "message": "기능 유형" }, - "fundYourWallet": { - "message": "지갑에 자금 추가" - }, - "fundYourWalletDescription": { - "message": "지갑에 $1의 자금을 추가하여 시작하세요.", - "description": "$1 is the token symbol" - }, "gas": { "message": "가스" }, @@ -1973,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": "뒤로 가기" }, @@ -4994,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": "상태 로그를 가져오는 도중 오류가 발생했습니다." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 7831e080500b..4c02a9dc223e 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Comprar agora" }, - "buyToken": { - "message": "Comprar $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index d32a603b367b..f1e5d27589c5 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Купить сейчас" }, - "buyToken": { - "message": "Купить $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Байты" }, @@ -1883,13 +1879,6 @@ "functionType": { "message": "Тип функции" }, - "fundYourWallet": { - "message": "Пополните свой кошелек" - }, - "fundYourWalletDescription": { - "message": "Начните с добавления $1 в свой кошелек.", - "description": "$1 is the token symbol" - }, "gas": { "message": "Газ" }, @@ -1973,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": "Назад" }, @@ -4994,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": "Ошибка при получении журналов состояния." }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 0c3675d78754..76e91829fc2c 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Bilhin Ngayon" }, - "buyToken": { - "message": "Bumili ng $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bytes" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 253e00845388..3b1899614d70 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Şimdi Satın Al" }, - "buyToken": { - "message": "$1 Al", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Bayt" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 538108c0677d..4bfcba6dac1f 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "Mua ngay" }, - "buyToken": { - "message": "Mua $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "Byte" }, @@ -1883,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" }, @@ -1973,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" }, @@ -4994,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." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index d3a4af11220a..80a31d532482 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -805,10 +805,6 @@ "buyNow": { "message": "立即购买" }, - "buyToken": { - "message": "购买 $1", - "description": "$1 is the token symbol" - }, "bytes": { "message": "字节" }, @@ -1883,13 +1879,6 @@ "functionType": { "message": "功能类型" }, - "fundYourWallet": { - "message": "向您的钱包存入资金" - }, - "fundYourWalletDescription": { - "message": "将一些 $1 添加到您的钱包并开始使用", - "description": "$1 is the token symbol" - }, "gas": { "message": "燃料" }, @@ -1973,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": "返回" }, @@ -4994,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": "检索状态日志时出错。" }, 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/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/alert-controller.test.ts b/app/scripts/controllers/alert-controller.test.ts index a8aee606e02d..de314c31f050 100644 --- a/app/scripts/controllers/alert-controller.test.ts +++ b/app/scripts/controllers/alert-controller.test.ts @@ -2,16 +2,16 @@ * @jest-environment node */ import { ControllerMessenger } from '@metamask/base-controller'; -import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; -import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { EthAccountType } from '@metamask/keyring-api'; import { - AlertControllerActions, - AlertControllerEvents, AlertController, AllowedActions, AllowedEvents, - AlertControllerState, + AlertControllerMessenger, + AlertControllerGetStateAction, + AlertControllerStateChangeEvent, + AlertControllerOptions, + getDefaultAlertControllerState, } from './alert-controller'; const EMPTY_ACCOUNT = { @@ -28,230 +28,153 @@ const EMPTY_ACCOUNT = { importTime: 0, }, }; -describe('AlertController', () => { - let controllerMessenger: ControllerMessenger< - AlertControllerActions | AllowedActions, - | AlertControllerEvents - | KeyringControllerStateChangeEvent - | SnapControllerStateChangeEvent - | AllowedEvents + +type WithControllerOptions = Partial; + +type WithControllerCallback = ({ + controller, +}: { + controller: AlertController; + messenger: ControllerMessenger< + AllowedActions | AlertControllerGetStateAction, + AllowedEvents | AlertControllerStateChangeEvent >; - let alertController: AlertController; +}) => ReturnValue; + +type WithControllerArgs = + | [WithControllerCallback] + | [WithControllerOptions, WithControllerCallback]; - beforeEach(() => { - controllerMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents - >(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => EMPTY_ACCOUNT, - ); +async function withController( + ...args: WithControllerArgs +): Promise { + const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; + const { ...alertControllerOptions } = rest; - const alertMessenger = controllerMessenger.getRestricted({ + const controllerMessenger = new ControllerMessenger< + AllowedActions | AlertControllerGetStateAction, + AllowedEvents | AlertControllerStateChangeEvent + >(); + + const alertControllerMessenger: AlertControllerMessenger = + controllerMessenger.getRestricted({ name: 'AlertController', - allowedActions: [`AccountsController:getSelectedAccount`], - allowedEvents: [`AccountsController:selectedAccountChange`], + allowedActions: ['AccountsController:getSelectedAccount'], + allowedEvents: ['AccountsController:selectedAccountChange'], }); - alertController = new AlertController({ - state: { - unconnectedAccountAlertShownOrigins: { - testUnconnectedOrigin: false, - }, - web3ShimUsageOrigins: { - testWeb3ShimUsageOrigin: 0, - }, - }, - controllerMessenger: alertMessenger, - }); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + jest.fn().mockReturnValue(EMPTY_ACCOUNT), + ); + + const controller = new AlertController({ + messenger: alertControllerMessenger, + ...alertControllerOptions, }); + return await fn({ + controller, + messenger: controllerMessenger, + }); +} + +describe('AlertController', () => { describe('default state', () => { - it('should be same as AlertControllerState initialized', () => { - expect(alertController.store.getState()).toStrictEqual({ - alertEnabledness: { - unconnectedAccount: true, - web3ShimUsage: true, - }, - unconnectedAccountAlertShownOrigins: { - testUnconnectedOrigin: false, - }, - web3ShimUsageOrigins: { - testWeb3ShimUsageOrigin: 0, - }, + it('should be same as AlertControllerState initialized', async () => { + await withController(({ controller }) => { + expect(controller.state).toStrictEqual( + getDefaultAlertControllerState(), + ); }); }); }); describe('alertEnabledness', () => { - it('should default unconnectedAccount of alertEnabledness to true', () => { - expect( - alertController.store.getState().alertEnabledness.unconnectedAccount, - ).toStrictEqual(true); + it('should default unconnectedAccount of alertEnabledness to true', async () => { + await withController(({ controller }) => { + expect( + controller.state.alertEnabledness.unconnectedAccount, + ).toStrictEqual(true); + }); }); - it('should set unconnectedAccount of alertEnabledness to false', () => { - alertController.setAlertEnabledness('unconnectedAccount', false); - expect( - alertController.store.getState().alertEnabledness.unconnectedAccount, - ).toStrictEqual(false); - expect( - controllerMessenger.call('AlertController:getState').alertEnabledness - .unconnectedAccount, - ).toStrictEqual(false); + it('should set unconnectedAccount of alertEnabledness to false', async () => { + await withController(({ controller }) => { + controller.setAlertEnabledness('unconnectedAccount', false); + expect( + controller.state.alertEnabledness.unconnectedAccount, + ).toStrictEqual(false); + }); }); }); describe('unconnectedAccountAlertShownOrigins', () => { - it('should default unconnectedAccountAlertShownOrigins', () => { - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: false, - }); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: false, + it('should default unconnectedAccountAlertShownOrigins', async () => { + await withController(({ controller }) => { + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); }); }); - it('should set unconnectedAccountAlertShownOrigins', () => { - alertController.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: true, - }); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: true, + it('should set unconnectedAccountAlertShownOrigins', async () => { + await withController(({ controller }) => { + controller.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: true, + }); }); }); }); describe('web3ShimUsageOrigins', () => { - it('should default web3ShimUsageOrigins', () => { - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 0, - }); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 0, + it('should default web3ShimUsageOrigins', async () => { + await withController(({ controller }) => { + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({}); }); }); - it('should set origin of web3ShimUsageOrigins to recorded', () => { - alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, - }); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, + it('should set origin of web3ShimUsageOrigins to recorded', async () => { + await withController(({ controller }) => { + controller.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); }); }); - it('should set origin of web3ShimUsageOrigins to dismissed', () => { - alertController.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 2, - }); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 2, + it('should set origin of web3ShimUsageOrigins to dismissed', async () => { + await withController(({ controller }) => { + controller.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({ + testWeb3ShimUsageOrigin: 2, + }); }); }); }); describe('selectedAccount change', () => { - it('should set unconnectedAccountAlertShownOrigins to {}', () => { - controllerMessenger.publish('AccountsController:selectedAccountChange', { - id: '', - address: '0x1234567', - options: {}, - methods: [], - type: 'eip155:eoa', - metadata: { - name: '', - keyring: { - type: '', + it('should set unconnectedAccountAlertShownOrigins to {}', async () => { + await withController(({ controller, messenger }) => { + messenger.publish('AccountsController:selectedAccountChange', { + id: '', + address: '0x1234567', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, }, - importTime: 0, - }, - }); - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({}); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({}); - }); - }); - - describe('AlertController:getState', () => { - it('should return the current state of the property', () => { - const defaultWeb3ShimUsageOrigins = { - testWeb3ShimUsageOrigin: 0, - }; - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual(defaultWeb3ShimUsageOrigins); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual(defaultWeb3ShimUsageOrigins); - }); - }); - - describe('AlertController:stateChange', () => { - it('state will be published when there is state change', () => { - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 0, - }); - - controllerMessenger.subscribe( - 'AlertController:stateChange', - (state: Partial) => { - expect(state.web3ShimUsageOrigins).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, - }); - }, - ); - - alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); - - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, - }); - expect( - alertController.getWeb3ShimUsageState('testWeb3ShimUsageOrigin'), - ).toStrictEqual(1); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, + }); + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); }); }); }); diff --git a/app/scripts/controllers/alert-controller.ts b/app/scripts/controllers/alert-controller.ts index 9e1882035e02..90e177e9edca 100644 --- a/app/scripts/controllers/alert-controller.ts +++ b/app/scripts/controllers/alert-controller.ts @@ -1,9 +1,13 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { TOGGLEABLE_ALERT_TYPES, Web3ShimUsageAlertStates, @@ -14,10 +18,10 @@ const controllerName = 'AlertController'; /** * Returns the state of the {@link AlertController}. */ -export type AlertControllerGetStateAction = { - type: 'AlertController:getState'; - handler: () => AlertControllerState; -}; +export type AlertControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AlertControllerState +>; /** * Actions exposed by the {@link AlertController}. @@ -27,10 +31,10 @@ export type AlertControllerActions = AlertControllerGetStateAction; /** * Event emitted when the state of the {@link AlertController} changes. */ -export type AlertControllerStateChangeEvent = { - type: 'AlertController:stateChange'; - payload: [AlertControllerState, []]; -}; +export type AlertControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + AlertControllerState +>; /** * Events emitted by {@link AlertController}. @@ -76,12 +80,15 @@ export type AlertControllerState = { * @property state - The initial controller state * @property controllerMessenger - The controller messenger */ -type AlertControllerOptions = { +export type AlertControllerOptions = { state?: Partial; - controllerMessenger: AlertControllerMessenger; + messenger: AlertControllerMessenger; }; -const defaultState: AlertControllerState = { +/** + * Function to get default state of the {@link AlertController}. + */ +export const getDefaultAlertControllerState = (): AlertControllerState => ({ alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( (alertEnabledness: Record, alertType: string) => { alertEnabledness[alertType] = true; @@ -91,61 +98,76 @@ const defaultState: AlertControllerState = { ), unconnectedAccountAlertShownOrigins: {}, web3ShimUsageOrigins: {}, +}); + +/** + * {@link AlertController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + alertEnabledness: { + persist: true, + anonymous: true, + }, + unconnectedAccountAlertShownOrigins: { + persist: true, + anonymous: false, + }, + web3ShimUsageOrigins: { + persist: true, + anonymous: false, + }, }; /** * Controller responsible for maintaining alert-related state. */ -export class AlertController { - store: ObservableStore; - - readonly #controllerMessenger: AlertControllerMessenger; - +export class AlertController extends BaseController< + typeof controllerName, + AlertControllerState, + AlertControllerMessenger +> { #selectedAddress: string; constructor(opts: AlertControllerOptions) { - const state: AlertControllerState = { - ...defaultState, - ...opts.state, - }; - - this.store = new ObservableStore(state); - this.#controllerMessenger = opts.controllerMessenger; - this.#controllerMessenger.registerActionHandler( - 'AlertController:getState', - () => this.store.getState(), - ); - this.store.subscribe((alertState: AlertControllerState) => { - this.#controllerMessenger.publish( - 'AlertController:stateChange', - alertState, - [], - ); + super({ + messenger: opts.messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultAlertControllerState(), + ...opts.state, + }, }); - this.#selectedAddress = this.#controllerMessenger.call( + this.#selectedAddress = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ).address; - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', (account: { address: string }) => { - const currentState = this.store.getState(); + const currentState = this.state; if ( currentState.unconnectedAccountAlertShownOrigins && this.#selectedAddress !== account.address ) { this.#selectedAddress = account.address; - this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); + this.update((state) => { + state.unconnectedAccountAlertShownOrigins = {}; + }); } }, ); } setAlertEnabledness(alertId: string, enabledness: boolean): void { - const { alertEnabledness } = this.store.getState(); - alertEnabledness[alertId] = enabledness; - this.store.updateState({ alertEnabledness }); + this.update((state) => { + state.alertEnabledness[alertId] = enabledness; + }); } /** @@ -154,9 +176,9 @@ export class AlertController { * @param origin - The origin the alert has been shown for */ setUnconnectedAccountAlertShown(origin: string): void { - const { unconnectedAccountAlertShownOrigins } = this.store.getState(); - unconnectedAccountAlertShownOrigins[origin] = true; - this.store.updateState({ unconnectedAccountAlertShownOrigins }); + this.update((state) => { + state.unconnectedAccountAlertShownOrigins[origin] = true; + }); } /** @@ -167,7 +189,7 @@ export class AlertController { * origin, or undefined. */ getWeb3ShimUsageState(origin: string): number | undefined { - return this.store.getState().web3ShimUsageOrigins?.[origin]; + return this.state.web3ShimUsageOrigins?.[origin]; } /** @@ -194,10 +216,10 @@ export class AlertController { * @param value - The state value to set. */ #setWeb3ShimUsageState(origin: string, value: number): void { - const { web3ShimUsageOrigins } = this.store.getState(); - if (web3ShimUsageOrigins) { - web3ShimUsageOrigins[origin] = value; - this.store.updateState({ web3ShimUsageOrigins }); - } + this.update((state) => { + if (state.web3ShimUsageOrigins) { + state.web3ShimUsageOrigins[origin] = value; + } + }); } } diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 25b6eae98c33..35449cb40764 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -2,6 +2,10 @@ import nock from 'nock'; import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps'; +import { flushPromises } from '../../../../test/lib/timer-helpers'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import * as bridgeUtil from '../../../../ui/pages/bridge/bridge.util'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; @@ -26,9 +30,15 @@ describe('BridgeController', function () { beforeEach(() => { jest.clearAllMocks(); + jest.clearAllTimers(); + nock(BRIDGE_API_BASE_URL) .get('/getAllFeatureFlags') .reply(200, { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + }, 'extension-support': true, 'src-network-allowlist': [10, 534352], 'dest-network-allowlist': [137, 42161], @@ -55,6 +65,7 @@ describe('BridgeController', function () { symbol: 'ABC', }, ]); + bridgeController.resetState(); }); it('constructor should setup correctly', function () { @@ -66,13 +77,35 @@ describe('BridgeController', function () { extensionSupport: true, destNetworkAllowlist: [CHAIN_IDS.POLYGON, CHAIN_IDS.ARBITRUM], srcNetworkAllowlist: [CHAIN_IDS.OPTIMISM, CHAIN_IDS.SCROLL], + extensionConfig: { + maxRefreshCount: 1, + refreshRate: 3, + }, }; expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + const setIntervalLengthSpy = jest.spyOn( + bridgeController, + 'setIntervalLength', + ); + await bridgeController.setBridgeFeatureFlags(); expect(bridgeController.state.bridgeState.bridgeFeatureFlags).toStrictEqual( expectedFeatureFlagsResponse, ); + expect(setIntervalLengthSpy).toHaveBeenCalledTimes(1); + expect(setIntervalLengthSpy).toHaveBeenCalledWith(3); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + bridgeFeatureFlags: expectedFeatureFlagsResponse, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); }); it('selectDestNetwork should set the bridge dest tokens and top assets', async function () { @@ -94,6 +127,11 @@ describe('BridgeController', function () { expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, ]); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); }); it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { @@ -118,5 +156,240 @@ describe('BridgeController', function () { symbol: 'ABC', }, ]); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should update the quoteRequest state', function () { + bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: 10, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: undefined, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: undefined, + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: '0x2ABC', + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x2ABC', + walletAddress: undefined, + }); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should trigger quote polling if request is valid', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingByNetworkClientIdSpy = jest.spyOn( + bridgeController, + 'startPollingByNetworkClientId', + ); + messengerMock.call.mockReturnValue({ address: '0x123' } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(bridgeUtil, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([1, 2, 3] as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([5, 6, 7] as never); + }, 10000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((_, reject) => { + return setTimeout(() => { + reject(new Error('Network error')); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledWith( + '1', + quoteRequest, + ); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith(quoteRequest); + + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched ?? 0; + expect(firstFetchTime).toBeGreaterThan(0); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [1, 2, 3], + quotesLoadingStatus: 1, + }), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + firstFetchTime, + ); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [5, 6, 7], + quotesLoadingStatus: 1, + }), + ); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(secondFetchTime).toBeGreaterThan(firstFetchTime); + + // After 3nd fetch throws an error + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: [5, 6, 7], + quotesLoadingStatus: 2, + }), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + secondFetchTime, + ); + }); + + it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () { + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingByNetworkClientIdSpy = jest.spyOn( + bridgeController, + 'startPollingByNetworkClientId', + ); + messengerMock.call.mockReturnValueOnce({ address: '0x123' } as never); + + bridgeController.updateBridgeQuoteRequestParams({ + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + }); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingByNetworkClientIdSpy).not.toHaveBeenCalled(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + destChainId: 10, + destTokenAddress: '0x123', + }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 841d735ac52c..1d20e6f404e4 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -1,7 +1,10 @@ -import { BaseController, StateMetadata } from '@metamask/base-controller'; +import { StateMetadata } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { NetworkClientId } from '@metamask/network-controller'; import { fetchBridgeFeatureFlags, + fetchBridgeQuotes, fetchBridgeTokens, // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -9,11 +12,24 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; +import { decimalToHex } from '../../../../shared/modules/conversion.utils'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { QuoteRequest } from '../../../../ui/pages/bridge/types'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { isValidQuoteRequest } from '../../../../ui/pages/bridge/utils/quote'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, + REFRESH_INTERVAL_MS, + RequestStatus, } from './constants'; -import { BridgeControllerState, BridgeControllerMessenger } from './types'; +import { + BridgeControllerState, + BridgeControllerMessenger, + BridgeFeatureFlagsKey, +} from './types'; const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { bridgeState: { @@ -22,7 +38,7 @@ const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { }, }; -export default class BridgeController extends BaseController< +export default class BridgeController extends StaticIntervalPollingController< typeof BRIDGE_CONTROLLER_NAME, { bridgeState: BridgeControllerState }, BridgeControllerMessenger @@ -32,9 +48,13 @@ export default class BridgeController extends BaseController< name: BRIDGE_CONTROLLER_NAME, metadata, messenger, - state: { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE }, + state: { + bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, + }, }); + this.setIntervalLength(REFRESH_INTERVAL_MS); + this.messagingSystem.registerActionHandler( `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), @@ -47,12 +67,55 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, this.selectDestNetwork.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:updateBridgeQuoteRequestParams`, + this.updateBridgeQuoteRequestParams.bind(this), + ); } + _executePoll = async ( + _: NetworkClientId, + updatedQuoteRequest: QuoteRequest, + ) => { + await this.#fetchBridgeQuotes(updatedQuoteRequest); + }; + + updateBridgeQuoteRequestParams = (paramsToUpdate: Partial) => { + this.stopAllPolling(); + const { bridgeState } = this.state; + const updatedQuoteRequest = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, + ...paramsToUpdate, + }; + + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quoteRequest: updatedQuoteRequest, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }; + }); + + if (isValidQuoteRequest(updatedQuoteRequest)) { + const walletAddress = this.#getSelectedAccount().address; + this.startPollingByNetworkClientId( + decimalToHex(updatedQuoteRequest.srcChainId), + { ...updatedQuoteRequest, walletAddress }, + ); + } + }; + resetState = () => { + this.stopAllPolling(); this.update((_state) => { _state.bridgeState = { + ..._state.bridgeState, ...DEFAULT_BRIDGE_CONTROLLER_STATE, + quotes: [], + bridgeFeatureFlags: _state.bridgeState.bridgeFeatureFlags, }; }); }; @@ -63,6 +126,9 @@ export default class BridgeController extends BaseController< this.update((_state) => { _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; }); + this.setIntervalLength( + bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].refreshRate, + ); }; selectSrcNetwork = async (chainId: Hex) => { @@ -75,6 +141,36 @@ export default class BridgeController extends BaseController< await this.#setTokens(chainId, 'destTokens'); }; + #fetchBridgeQuotes = async (request: QuoteRequest) => { + const { bridgeState } = this.state; + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.LOADING, + }; + }); + + try { + const quotes = await fetchBridgeQuotes(request); + this.update((_state) => { + _state.bridgeState = { + ..._state.bridgeState, + quotes, + quotesLoadingStatus: RequestStatus.FETCHED, + }; + }); + } catch (error) { + console.log('Failed to fetch bridge quotes', error); + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quotesLoadingStatus: RequestStatus.ERROR, + }; + }); + } + }; + #setTopAssets = async ( chainId: Hex, stateKey: 'srcTopAssets' | 'destTopAssets', @@ -93,4 +189,8 @@ export default class BridgeController extends BaseController< _state.bridgeState = { ...bridgeState, [stateKey]: tokens }; }); }; + + #getSelectedAccount() { + return this.messagingSystem.call('AccountsController:getSelectedAccount'); + } } diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index 58c7d015b7bb..a4aa3264fdc8 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -1,9 +1,23 @@ +import { zeroAddress } from 'ethereumjs-util'; import { BridgeControllerState, BridgeFeatureFlagsKey } from './types'; export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; +export const REFRESH_INTERVAL_MS = 30 * 1000; +const DEFAULT_MAX_REFRESH_COUNT = 5; +const DEFAULT_SLIPPAGE = 0.5; + +export enum RequestStatus { + LOADING, + FETCHED, + ERROR, +} export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, + }, [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], @@ -12,4 +26,12 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { srcTopAssets: [], destTokens: {}, destTopAssets: [], + quoteRequest: { + walletAddress: undefined, + srcTokenAddress: zeroAddress(), + slippage: DEFAULT_SLIPPAGE, + }, + quotes: [], + quotesLastFetched: undefined, + quotesLoadingStatus: undefined, }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index 2fb36e1e983e..10c2d8646545 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -3,17 +3,26 @@ import { RestrictedControllerMessenger, } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import { SwapsTokenObject } from '../../../../shared/constants/swaps'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { QuoteRequest, QuoteResponse } from '../../../../ui/pages/bridge/types'; import BridgeController from './bridge-controller'; -import { BRIDGE_CONTROLLER_NAME } from './constants'; +import { BRIDGE_CONTROLLER_NAME, RequestStatus } from './constants'; export enum BridgeFeatureFlagsKey { + EXTENSION_CONFIG = 'extensionConfig', EXTENSION_SUPPORT = 'extensionSupport', NETWORK_SRC_ALLOWLIST = 'srcNetworkAllowlist', NETWORK_DEST_ALLOWLIST = 'destNetworkAllowlist', } export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + }; [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: boolean; [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: Hex[]; [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: Hex[]; @@ -25,11 +34,16 @@ export type BridgeControllerState = { srcTopAssets: { address: string }[]; destTokens: Record; destTopAssets: { address: string }[]; + quoteRequest: Partial; + quotes: QuoteResponse[]; + quotesLastFetched?: number; + quotesLoadingStatus?: RequestStatus; }; export enum BridgeUserAction { SELECT_SRC_NETWORK = 'selectSrcNetwork', SELECT_DEST_NETWORK = 'selectDestNetwork', + UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', } export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', @@ -44,20 +58,24 @@ type BridgeControllerAction = { type BridgeControllerActions = | BridgeControllerAction | BridgeControllerAction - | BridgeControllerAction; + | BridgeControllerAction + | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, BridgeControllerState >; +type AllowedActions = AccountsControllerGetSelectedAccountAction['type']; +type AllowedEvents = never; + /** * The messenger for the BridgeController. */ export type BridgeControllerMessenger = RestrictedControllerMessenger< typeof BRIDGE_CONTROLLER_NAME, - BridgeControllerActions, + BridgeControllerActions | AccountsControllerGetSelectedAccountAction, BridgeControllerEvents, - never, - never + AllowedActions, + AllowedEvents >; diff --git a/app/scripts/controllers/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 4e68384f1e51..a4b91a8d3b1a 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 64ce69d36143..e1cdb2e8a4f6 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; }; @@ -218,12 +220,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/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts index 063271a9984a..09893ea05a5e 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts @@ -1,12 +1,11 @@ import { jsonrpc2, Json } from '@metamask/utils'; import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; -import type { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware, { EvmMethodsToNonEvmAccountFilterMessenger, } from './createEvmMethodsToNonEvmAccountReqFilterMiddleware'; describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { - const getMockRequest = (method: string, params: Json) => ({ + const getMockRequest = (method: string, params: Record) => ({ jsonrpc: jsonrpc2, id: 1, method, @@ -286,7 +285,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { }: { accountType: EthAccountType | BtcAccountType; method: string; - params: Json; + params: Record; calledNext: number; }) => { const filterFn = createEvmMethodsToNonEvmAccountReqFilterMiddleware({ @@ -298,7 +297,7 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { const mockEnd = jest.fn(); filterFn( - getMockRequest(method, params) as JsonRpcRequest, + getMockRequest(method, params), getMockResponse(), mockNext, mockEnd, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index a1c5a036f13f..cb57c681649f 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -53,6 +53,8 @@ const RATE_LIMIT_MAP = { [MESSAGE_TYPE.ETH_DECRYPT]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, + [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, + [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: RATE_LIMIT_TYPES.TIMEOUT, [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: RATE_LIMIT_TYPES.TIMEOUT, [MESSAGE_TYPE.SEND_METADATA]: RATE_LIMIT_TYPES.BLOCKED, @@ -126,6 +128,8 @@ const EVENT_NAME_MAP = { */ const TRANSFORM_PARAMS_MAP = { [MESSAGE_TYPE.WATCH_ASSET]: ({ type }) => ({ type }), + [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN]: ([{ chainId }]) => ({ chainId }), + [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN]: ([{ chainId }]) => ({ chainId }), }; const rateLimitTimeoutsByMethod = {}; diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 01daaf2974a4..244a995bf5f7 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -883,6 +883,34 @@ describe('createRPCMethodTrackingMiddleware', () => { }, { type: 'ERC20' }, ], + [ + 'only the chain ID', + 'wallet_addEthereumChain', + [ + { + chainId: '0x64', + chainName: 'Gnosis', + rpcUrls: ['https://rpc.gnosischain.com'], + iconUrls: [ + 'https://xdaichain.com/fake/example/url/xdai.svg', + 'https://xdaichain.com/fake/example/url/xdai.png', + ], + nativeCurrency: { + name: 'XDAI', + symbol: 'XDAI', + decimals: 18, + }, + blockExplorerUrls: ['https://blockscout.com/poa/xdai/'], + }, + ], + { chainId: '0x64' }, + ], + [ + 'only the chain ID', + 'wallet_switchEthereumChain', + [{ chainId: '0x123' }], + { chainId: '0x123' }, + ], ])( `should include %s in the '%s' tracked events params property`, async (_, method, params, expected) => { diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 7eb8dc0cc5a2..ebfdbe3f04d7 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -86,12 +86,11 @@ export function createPPOMMiddleware< return; } - const isSupportedChain = await isChainSupported(chainId); - if ( !securityAlertsEnabled || !CONFIRMATION_METHODS.includes(req.method) || - !isSupportedChain + // Do not move this call above this check because it will result in unnecessary calls + !(await isChainSupported(chainId)) ) { return; } diff --git a/app/scripts/lib/ppom/security-alerts-api.test.ts b/app/scripts/lib/ppom/security-alerts-api.test.ts index 9d2d97652d4f..460139c1d359 100644 --- a/app/scripts/lib/ppom/security-alerts-api.test.ts +++ b/app/scripts/lib/ppom/security-alerts-api.test.ts @@ -27,6 +27,8 @@ const RESPONSE_MOCK = { description: 'Test Description', }; +const BASE_URL = 'https://example.com'; + describe('Security Alerts API', () => { const fetchMock = jest.fn(); @@ -40,7 +42,7 @@ describe('Security Alerts API', () => { json: async () => RESPONSE_MOCK, }); - process.env.SECURITY_ALERTS_API_URL = 'https://example.com'; + process.env.SECURITY_ALERTS_API_URL = BASE_URL; }); describe('validateWithSecurityAlertsAPI', () => { @@ -54,8 +56,14 @@ describe('Security Alerts API', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith( - `https://example.com/validate/${CHAIN_ID_MOCK}`, - expect.any(Object), + `${BASE_URL}/validate/${CHAIN_ID_MOCK}`, + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(REQUEST_MOCK), + headers: { + 'Content-Type': 'application/json', + }, + }), ); }); @@ -101,7 +109,7 @@ describe('Security Alerts API', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith( - `https://example.com/supportedChains`, + `${BASE_URL}/supportedChains`, undefined, ); }); diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 1b9e9f4ddbfc..354bb0bbb620 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -424,12 +424,17 @@ export function rewriteReport(report) { if (!report.extra) { report.extra = {}; } - - report.extra.appState = appState; - if (browser.runtime && browser.runtime.id) { - report.extra.extensionId = browser.runtime.id; + if (!report.tags) { + report.tags = {}; } - report.extra.installType = installType; + + Object.assign(report.extra, { + appState, + installType, + extensionId: browser.runtime?.id, + }); + + report.tags.installType = installType; } catch (err) { log('Error rewriting report', err); } diff --git a/app/scripts/lib/transaction/metrics.test.ts b/app/scripts/lib/transaction/metrics.test.ts index 1ff509da3c09..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,8 @@ 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, }; @@ -166,6 +168,7 @@ describe('Transaction metrics', () => { first_seen: 1624408066355, gas_limit: '0x7b0d', gas_price: '2', + 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 7df95ec66a05..b33be10c8df4 100644 --- a/app/scripts/lib/transaction/metrics.ts +++ b/app/scripts/lib/transaction/metrics.ts @@ -916,6 +916,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 +929,7 @@ async function buildEventFragmentProperties({ } else if (contractInteractionTypes) { transactionType = TransactionType.contractInteraction; transactionContractMethod = contractMethodName; + transactionContractAddress = transactionMeta.txParams?.to; if ( transactionContractMethod === contractMethodNames.APPROVE && tokenStandard === TokenStandard.ERC20 @@ -1088,6 +1090,7 @@ async function buildEventFragmentProperties({ first_seen: time, gas_limit: gasLimit, transaction_replaced: transactionReplaced, + transaction_contract_address: transactionContractAddress, ...extraParams, ...gasParamsInGwei, // TODO: Replace `any` with type diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 72b1875f7491..a85233ca5119 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -156,7 +156,11 @@ import { NotificationServicesPushController, NotificationServicesController, } from '@metamask/notification-services-controller'; -import { methodsRequiringNetworkSwitch } from '../../shared/constants/methods-tags'; +import { + methodsRequiringNetworkSwitch, + methodsThatCanSwitchNetworkWithoutApproval, + methodsThatShouldBeEnqueued, +} from '../../shared/constants/methods-tags'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; @@ -485,22 +489,6 @@ export default class MetamaskController extends EventEmitter { this.approvalController.clear(providerErrors.userRejectedRequest()); }; - this.queuedRequestController = new QueuedRequestController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'QueuedRequestController', - allowedActions: [ - 'NetworkController:getState', - 'NetworkController:setActiveNetwork', - 'SelectedNetworkController:getNetworkClientIdForDomain', - ], - allowedEvents: ['SelectedNetworkController:stateChange'], - }), - shouldRequestSwitchNetwork: ({ method }) => - methodsRequiringNetworkSwitch.includes(method), - clearPendingConfirmations, - showApprovalRequest: opts.showUserConfirmation, - }); - this.approvalController = new ApprovalController({ messenger: this.controllerMessenger.getRestricted({ name: 'ApprovalController', @@ -516,6 +504,28 @@ export default class MetamaskController extends EventEmitter { ], }); + this.queuedRequestController = new QueuedRequestController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'QueuedRequestController', + allowedActions: [ + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', + 'SelectedNetworkController:getNetworkClientIdForDomain', + ], + allowedEvents: ['SelectedNetworkController:stateChange'], + }), + shouldRequestSwitchNetwork: ({ method }) => + methodsRequiringNetworkSwitch.includes(method), + canRequestSwitchNetworkWithoutApproval: ({ method }) => + methodsThatCanSwitchNetworkWithoutApproval.includes(method), + clearPendingConfirmations, + showApprovalRequest: () => { + if (this.approvalController.getTotalApprovalCount() > 0) { + opts.showUserConfirmation(); + } + }, + }); + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) this.mmiConfigurationController = new MmiConfigurationController({ initState: initState.MmiConfigurationController, @@ -867,13 +877,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, @@ -997,6 +1007,7 @@ export default class MetamaskController extends EventEmitter { state: initState.TokenRatesController, messenger: tokenRatesMessenger, tokenPricesService: new CodefiTokenPricesServiceV2(), + disabled: !this.preferencesController.state.useCurrencyRateCheck, }); this.controllerMessenger.subscribe( @@ -1005,9 +1016,9 @@ export default class MetamaskController extends EventEmitter { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; if (currUseCurrencyRateCheck && !prevUseCurrencyRateCheck) { - this.tokenRatesController.start(); + this.tokenRatesController.enable(); } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { - this.tokenRatesController.stop(); + this.tokenRatesController.disable(); } }, this.preferencesController.state), ); @@ -1787,7 +1798,7 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ state: initState.AlertController, - controllerMessenger: this.controllerMessenger.getRestricted({ + messenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], allowedActions: ['AccountsController:getSelectedAccount'], @@ -1979,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, }); @@ -2111,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({ @@ -2375,7 +2384,7 @@ export default class MetamaskController extends EventEmitter { AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, NetworkController: this.networkController, - AlertController: this.alertController.store, + AlertController: this.alertController, OnboardingController: this.onboardingController, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController, @@ -2430,7 +2439,7 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, - AlertController: this.alertController.store, + AlertController: this.alertController, OnboardingController: this.onboardingController, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController, @@ -2582,12 +2591,6 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.state; - const { useCurrencyRateCheck } = preferencesControllerState; - - if (useCurrencyRateCheck) { - this.tokenRatesController.start(); - } - if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.start(); } @@ -2600,12 +2603,6 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.state; - const { useCurrencyRateCheck } = preferencesControllerState; - - if (useCurrencyRateCheck) { - this.tokenRatesController.stop(); - } - if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.stop(); } @@ -3242,6 +3239,7 @@ export default class MetamaskController extends EventEmitter { backup, approvalController, phishingController, + tokenRatesController, // Notification Controllers authenticationController, userStorageController, @@ -3939,6 +3937,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( @@ -3999,15 +4002,21 @@ export default class MetamaskController extends EventEmitter { ), // CurrencyRateController - currencyRateStartPollingByNetworkClientId: - currencyRateController.startPollingByNetworkClientId.bind( - currencyRateController, - ), + currencyRateStartPolling: currencyRateController.startPolling.bind( + currencyRateController, + ), currencyRateStopPollingByPollingToken: currencyRateController.stopPollingByPollingToken.bind( currencyRateController, ), + tokenRatesStartPolling: + tokenRatesController.startPolling.bind(tokenRatesController), + tokenRatesStopPollingByPollingToken: + tokenRatesController.stopPollingByPollingToken.bind( + tokenRatesController, + ), + // GasFeeController gasFeeStartPollingByNetworkClientId: gasFeeController.startPollingByNetworkClientId.bind(gasFeeController), @@ -5639,7 +5648,7 @@ export default class MetamaskController extends EventEmitter { this.preferencesController, ), shouldEnqueueRequest: (request) => { - return methodsRequiringNetworkSwitch.includes(request.method); + return methodsThatShouldBeEnqueued.includes(request.method); }, }); engine.push(requestQueueMiddleware); @@ -6633,12 +6642,13 @@ export default class MetamaskController extends EventEmitter { /** * A method that is called by the background when all instances of metamask are closed. - * Currently used to stop polling in the gasFeeController. + * Currently used to stop controller polling. */ onClientClosed() { try { this.gasFeeController.stopAllPolling(); this.currencyRateController.stopAllPolling(); + this.tokenRatesController.stopAllPolling(); this.appStateController.clearPollingTokens(); } catch (error) { console.error(error); diff --git a/app/scripts/services/data-deletion-service.ts b/app/scripts/services/data-deletion-service.ts index 3bdafc03b582..5ec9ede75a87 100644 --- a/app/scripts/services/data-deletion-service.ts +++ b/app/scripts/services/data-deletion-service.ts @@ -12,11 +12,17 @@ import { import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import { DeleteRegulationStatus } from '../../../shared/constants/metametrics'; -const DEFAULT_ANALYTICS_DATA_DELETION_SOURCE_ID = - process.env.ANALYTICS_DATA_DELETION_SOURCE_ID ?? 'test'; -const DEFAULT_ANALYTICS_DATA_DELETION_ENDPOINT = - process.env.ANALYTICS_DATA_DELETION_ENDPOINT ?? - 'https://metametrics.metamask.test'; +const inTest = process.env.IN_TEST; +const fallbackSourceId = 'test'; +const fallbackDataDeletionEndpoint = 'https://metametrics.metamask.test'; + +const DEFAULT_ANALYTICS_DATA_DELETION_SOURCE_ID = inTest + ? fallbackSourceId + : process.env.ANALYTICS_DATA_DELETION_SOURCE_ID ?? fallbackSourceId; +const DEFAULT_ANALYTICS_DATA_DELETION_ENDPOINT = inTest + ? fallbackDataDeletionEndpoint + : process.env.ANALYTICS_DATA_DELETION_ENDPOINT ?? + fallbackDataDeletionEndpoint; /** * The number of times we retry a specific failed request to the data deletion API. diff --git a/builds.yml b/builds.yml index 3bd1606e5ec4..824e08e32167 100644 --- a/builds.yml +++ b/builds.yml @@ -273,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/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.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 f4484f20763d..ac719964d896 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -674,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, @@ -702,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": { @@ -756,15 +765,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1217,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": { @@ -1271,9 +1295,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/json-rpc-middleware-stream": { @@ -1986,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": { @@ -2132,64 +2183,13 @@ }, "@metamask/queued-request-controller": { "packages": { - "@metamask/queued-request-controller>@metamask/base-controller": true, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": { - "packages": { - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2261,8 +2261,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2308,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, @@ -2331,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, @@ -2379,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, @@ -2839,9 +2907,9 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, + "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2867,10 +2935,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@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/user-operation-controller": { @@ -2881,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, @@ -2901,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, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index f4484f20763d..ac719964d896 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -674,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, @@ -702,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": { @@ -756,15 +765,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1217,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": { @@ -1271,9 +1295,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/json-rpc-middleware-stream": { @@ -1986,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": { @@ -2132,64 +2183,13 @@ }, "@metamask/queued-request-controller": { "packages": { - "@metamask/queued-request-controller>@metamask/base-controller": true, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": { - "packages": { - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2261,8 +2261,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2308,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, @@ -2331,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, @@ -2379,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, @@ -2839,9 +2907,9 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, + "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2867,10 +2935,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@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/user-operation-controller": { @@ -2881,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, @@ -2901,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, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index f4484f20763d..ac719964d896 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -674,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, @@ -702,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": { @@ -756,15 +765,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1217,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": { @@ -1271,9 +1295,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/json-rpc-middleware-stream": { @@ -1986,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": { @@ -2132,64 +2183,13 @@ }, "@metamask/queued-request-controller": { "packages": { - "@metamask/queued-request-controller>@metamask/base-controller": true, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": { - "packages": { - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2261,8 +2261,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2308,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, @@ -2331,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, @@ -2379,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, @@ -2839,9 +2907,9 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, + "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2867,10 +2935,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@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/user-operation-controller": { @@ -2881,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, @@ -2901,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, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 026e9c44a2c2..1b4e98aae177 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -766,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, @@ -794,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": { @@ -848,15 +857,30 @@ }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/controller-utils>@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, "@metamask/ethjs>@metamask/ethjs-unit": true, - "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true } }, + "@metamask/controller-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { "console.error": true, @@ -1309,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": { @@ -1363,9 +1387,24 @@ }, "@metamask/json-rpc-engine": { "packages": { + "@metamask/json-rpc-engine>@metamask/utils": true, "@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true, - "@metamask/utils": true + "@metamask/safe-event-emitter": true + } + }, + "@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/json-rpc-middleware-stream": { @@ -2078,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": { @@ -2224,64 +2275,13 @@ }, "@metamask/queued-request-controller": { "packages": { - "@metamask/queued-request-controller>@metamask/base-controller": true, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, + "@metamask/base-controller": true, + "@metamask/json-rpc-engine": true, "@metamask/queued-request-controller>@metamask/utils": true, + "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true } }, - "@metamask/queued-request-controller>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine": { - "packages": { - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/queued-request-controller>@metamask/rpc-errors": true, - "@metamask/safe-event-emitter": true - } - }, - "@metamask/queued-request-controller>@metamask/json-rpc-engine>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors": { - "packages": { - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, - "@metamask/rpc-errors>fast-safe-stringify": true - } - }, - "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>@metamask/superstruct": true, - "@metamask/utils>@scure/base": true, - "@metamask/utils>pony-cause": true, - "@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2353,8 +2353,23 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/rpc-errors>fast-safe-stringify": true, - "@metamask/utils": true + "@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2400,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, @@ -2423,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, @@ -2471,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, @@ -2931,9 +2999,9 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, + "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "bn.js": true, "browserify>buffer": true, "eth-method-registry": true, @@ -2959,10 +3027,19 @@ "@swc/helpers>tslib": true } }, - "@metamask/transaction-controller>@metamask/rpc-errors": { + "@metamask/transaction-controller>@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/user-operation-controller": { @@ -2973,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, @@ -2993,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, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 8b96f3fcc0fc..5a607452526c 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -6281,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": { @@ -6291,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, diff --git a/package.json b/package.json index fa9e6d550996..cad47b45c8f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.5.0", + "version": "12.6.0", "private": true, "repository": { "type": "git", @@ -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", @@ -286,12 +286,12 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", - "@metamask/controller-utils": "^11.2.0", + "@metamask/controller-utils": "^11.4.0", "@metamask/design-tokens": "^4.0.0", "@metamask/ens-controller": "^13.0.0", "@metamask/ens-resolver-snap": "^0.1.2", @@ -327,25 +327,26 @@ "@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": "^2.0.0", + "@metamask/queued-request-controller": "^7.0.0", "@metamask/rate-limit-controller": "^6.0.0", "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^18.0.2", - "@metamask/signature-controller": "^20.0.0", + "@metamask/signature-controller": "^21.0.0", "@metamask/smart-transactions-controller": "^13.0.0", "@metamask/snaps-controllers": "^9.11.1", "@metamask/snaps-execution-environments": "^6.9.1", "@metamask/snaps-rpc-methods": "^11.5.0", "@metamask/snaps-sdk": "^6.9.0", "@metamask/snaps-utils": "^8.4.1", - "@metamask/transaction-controller": "^37.3.0", + "@metamask/transaction-controller": "^38.1.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", "@ngraveio/bc-ur": "^1.1.12", @@ -601,6 +602,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", @@ -610,6 +612,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", @@ -667,6 +670,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", @@ -740,7 +744,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/common.ts b/shared/constants/common.ts index f45ec8abd7e4..96d2b0c65b55 100644 --- a/shared/constants/common.ts +++ b/shared/constants/common.ts @@ -1,5 +1,59 @@ +import { CHAIN_IDS } from './network'; + export enum EtherDenomination { ETH = 'ETH', GWEI = 'GWEI', WEI = 'WEI', } + +const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; +const BSC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'BscScan'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Etherscan'; +const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/'; +const GOERLI_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Goerli Etherscan'; +const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; +const POLYGON_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'PolygonScan'; +const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; +const AVALANCHE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Snowtrace'; +const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/'; +const OPTIMISM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Optimism Explorer'; +const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/'; +const ARBITRUM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'ArbiScan'; +const ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL = 'https://explorer.zksync.io/'; +const ZKSYNC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Zksync Explorer'; +const LINEA_DEFAULT_BLOCK_EXPLORER_URL = 'https://lineascan.build/'; +const LINEA_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'LineaScan'; +const BASE_DEFAULT_BLOCK_EXPLORER_URL = 'https://basescan.org/'; +const BASE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'BaseScan'; + +type BlockExplorerUrlMap = { + [key: string]: string; +}; + +export const CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP: BlockExplorerUrlMap = { + [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_URL, +} as const; + +export const CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP: BlockExplorerUrlMap = + { + [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + } as const; diff --git a/shared/constants/methods-tags.ts b/shared/constants/methods-tags.ts index a35954769b1b..329c0d493244 100644 --- a/shared/constants/methods-tags.ts +++ b/shared/constants/methods-tags.ts @@ -16,3 +16,22 @@ export const methodsRequiringNetworkSwitch = [ 'eth_signTypedData_v4', 'personal_sign', ] as const; + +/** + * This is a list of methods that may change the globally selected network + * without prompting for user approval. For UI/UX reasons these type of + * requests must be treated specially in the QueuedRequestController. + */ +export const methodsThatCanSwitchNetworkWithoutApproval = [ + 'wallet_addEthereumChain', + 'wallet_switchEthereumChain', +]; + +/** + * This is a list of methods that require special handling and must + * be enqueued and processed by the QueuedRequestController. + */ +export const methodsThatShouldBeEnqueued = [ + ...methodsRequiringNetworkSwitch, + ...methodsThatCanSwitchNetworkWithoutApproval, +]; diff --git a/shared/constants/swaps.ts b/shared/constants/swaps.ts index 3868c7b6e2f0..8dfecccef6e6 100644 --- a/shared/constants/swaps.ts +++ b/shared/constants/swaps.ts @@ -49,10 +49,6 @@ export type SwapsTokenObject = { iconUrl: string; }; -type BlockExplorerUrlMap = { - [key: string]: string; -}; - export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { symbol: CURRENCY_SYMBOLS.ETH, name: 'Ether', @@ -174,17 +170,6 @@ export const TOKEN_API_BASE_URL = 'https://tokens.api.cx.metamask.io'; export const GAS_API_BASE_URL = 'https://gas.api.cx.metamask.io'; export const GAS_DEV_API_BASE_URL = 'https://gas.uat-api.cx.metamask.io'; -const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; -export const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; -const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/'; -const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; -const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; -const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/'; -const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/'; -const ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL = 'https://explorer.zksync.io/'; -export const LINEA_DEFAULT_BLOCK_EXPLORER_URL = 'https://lineascan.build/'; -const BASE_DEFAULT_BLOCK_EXPLORER_URL = 'https://basescan.org/'; - export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [ CHAIN_IDS.MAINNET, SWAPS_TESTNET_CHAIN_ID, @@ -298,20 +283,6 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, } as const; -export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP: BlockExplorerUrlMap = - { - [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_URL, - } as const; - export const ETHEREUM = 'ethereum'; export const POLYGON = 'polygon'; export const BSC = 'bsc'; diff --git a/test/data/confirmations/personal_sign.ts b/test/data/confirmations/personal_sign.ts index b0f135efa53d..69c3ff9f75bc 100644 --- a/test/data/confirmations/personal_sign.ts +++ b/test/data/confirmations/personal_sign.ts @@ -1,3 +1,4 @@ +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm'; export const PERSONAL_SIGN_SENDER_ADDRESS = @@ -5,6 +6,7 @@ export const PERSONAL_SIGN_SENDER_ADDRESS = export const unapprovedPersonalSignMsg = { id: '0050d5b0-c023-11ee-a0cb-3390a510a0ab', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: new Date().getTime(), type: 'personal_sign', @@ -20,6 +22,7 @@ export const unapprovedPersonalSignMsg = { export const signatureRequestSIWE = { id: '210ca3b0-1ccb-11ef-b096-89c4d726ebb5', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -57,6 +60,7 @@ export const signatureRequestSIWE = { export const SignatureRequestSIWEWithResources = { id: '210ca3b0-1ccb-11ef-b096-89c4d726ebb5', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index 7be24a1389c6..831d561f0cb2 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -1,9 +1,10 @@ -import { TransactionType } from '@metamask/transaction-controller'; +import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm'; export const unapprovedTypedSignMsgV1 = { id: '82ab2400-e2c6-11ee-9627-73cc88f00492', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -60,6 +61,7 @@ const rawMessageV3 = { export const unapprovedTypedSignMsgV3 = { id: '17e41af0-e073-11ee-9eec-5fd284826685', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -129,6 +131,7 @@ export const rawMessageV4 = { export const unapprovedTypedSignMsgV4 = { id: '0050d5b0-c023-11ee-a0cb-3390a510a0ab', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: new Date().getTime(), chainid: '0x5', @@ -145,6 +148,7 @@ export const unapprovedTypedSignMsgV4 = { export const orderSignatureMsg = { id: 'e5249ae0-4b6b-11ef-831f-65b48eb489ec', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { result_type: 'loading', reason: 'validation_in_progress', @@ -165,6 +169,7 @@ export const orderSignatureMsg = { export const permitSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -185,6 +190,7 @@ export const permitSignatureMsg = { export const permitNFTSignatureMsg = { id: 'c5067710-87cf-11ef-916c-71f266571322', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: 1728651190529, type: 'eth_signTypedData', @@ -200,6 +206,7 @@ export const permitNFTSignatureMsg = { export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -219,6 +226,7 @@ export const permitSignatureMsgWithNoDeadline = { export const permitBatchSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -239,6 +247,7 @@ export const permitBatchSignatureMsg = { export const permitSingleSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 654e915a1305..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/fixture-builder.js b/test/e2e/fixture-builder.js index 0cd05c1dde13..4d7e1873bff4 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -75,6 +75,7 @@ function onboardingFixture() { hideZeroBalanceTokens: false, showExtensionInFullSizeView: false, showFiatInTestnets: false, + privacyMode: false, showTestNetworks: false, smartTransactionsOptInStatus: false, showNativeTokenAsMainBalance: true, 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 02e9e0583a31..5eaf14b8360b 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -909,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 @@ -928,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({ 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/pages/dialog/edit-network.ts b/test/e2e/page-objects/pages/dialog/edit-network.ts new file mode 100644 index 000000000000..09a1dd70a5f9 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/edit-network.ts @@ -0,0 +1,54 @@ +import { Driver } from '../../../webdriver/driver'; + +class EditNetworkModal { + private driver: Driver; + + private readonly editModalNetworkNameInput = + '[data-testid="network-form-network-name"]'; + + private readonly editModalRpcDropDownButton = + '[data-testid="test-add-rpc-drop-down"]'; + + private readonly editModalSaveButton = { + text: 'Save', + tag: 'button', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.editModalNetworkNameInput, + this.editModalRpcDropDownButton, + this.editModalSaveButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for select network dialog to be loaded', + e, + ); + throw e; + } + console.log('Edit network dialog is loaded'); + } + + /** + * Selects an RPC from the dropdown in the edit network modal. + * + * @param rpcName - The name of the RPC to select. + */ + async selectRPCInEditNetworkModal(rpcName: string): Promise { + console.log(`Select RPC ${rpcName} in edit network modal`); + await this.driver.clickElement(this.editModalRpcDropDownButton); + await this.driver.clickElement({ + text: rpcName, + tag: 'button', + }); + await this.driver.clickElementAndWaitToDisappear(this.editModalSaveButton); + } +} + +export default EditNetworkModal; diff --git a/test/e2e/page-objects/pages/dialog/select-network.ts b/test/e2e/page-objects/pages/dialog/select-network.ts index 2c399a4118d8..bc20c42855ae 100644 --- a/test/e2e/page-objects/pages/dialog/select-network.ts +++ b/test/e2e/page-objects/pages/dialog/select-network.ts @@ -3,15 +3,15 @@ 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 addNetworkButton = '[data-testid="test-add-button"]'; private readonly closeButton = 'button[aria-label="Close"]'; + private readonly editNetworkButton = + '[data-testid="network-list-item-options-edit"]'; + + private readonly rpcUrlItem = '.select-rpc-url__item'; + private readonly searchInput = '[data-testid="network-redesign-modal-search-input"]'; @@ -20,6 +20,11 @@ class SelectNetwork { tag: 'h4', }; + private readonly selectRpcMessage = { + text: 'Select RPC URL', + tag: 'h4', + }; + private readonly toggleButton = '.toggle-button > div'; constructor(driver: Driver) { @@ -42,15 +47,9 @@ class SelectNetwork { 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 clickAddButton(): Promise { + console.log('Click Add Button'); + await this.driver.clickElementAndWaitToDisappear(this.addNetworkButton); } async clickCloseButton(): Promise { @@ -58,21 +57,68 @@ class SelectNetwork { 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'); + async openEditNetworkModal(): Promise { + console.log('Open edit network modal'); + await this.driver.clickElementAndWaitToDisappear(this.editNetworkButton); + } + + async openNetworkListOptions(chainId: string): Promise { + console.log(`Open network options for ${chainId} in network dialog`); + await this.driver.clickElement( + `[data-testid="network-list-item-options-button-${chainId}"]`, + ); + } + + async openNetworkRPC(chainId: string): Promise { + console.log(`Open network RPC ${chainId}`); await this.driver.clickElementAndWaitToDisappear( - '[data-testid="test-add-button"]', + `[data-testid="network-rpc-name-button-${chainId}"]`, + ); + await this.driver.waitForSelector(this.selectRpcMessage); + } + + async selectNetworkName(networkName: string): Promise { + console.log(`Click ${networkName}`); + const networkNameItem = `[data-testid="${networkName}"]`; + await this.driver.clickElementAndWaitToDisappear(networkNameItem); + } + + async selectRPC(rpcName: string): Promise { + console.log(`Select RPC ${rpcName} for network`); + await this.driver.waitForSelector(this.selectRpcMessage); + await this.driver.clickElementAndWaitToDisappear({ + text: rpcName, + tag: 'button', + }); + } + + async toggleShowTestNetwork(): Promise { + console.log('Toggle show test network in select network dialog'); + await this.driver.clickElement(this.toggleButton); + } + + async check_networkRPCNumber(expectedNumber: number): Promise { + console.log( + `Wait for ${expectedNumber} RPC URLs to be displayed in select network dialog`, ); + await this.driver.wait(async () => { + const rpcNumber = await this.driver.findElements(this.rpcUrlItem); + return rpcNumber.length === expectedNumber; + }, 10000); + console.log(`${expectedNumber} RPC URLs found in select network dialog`); + } + + async check_rpcIsSelected(rpcName: string): Promise { + console.log(`Check RPC ${rpcName} is selected in network dialog`); + await this.driver.waitForSelector({ + text: rpcName, + tag: 'button', + }); } } diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 7c322b0f2cbb..101e6f9de83c 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -22,6 +22,11 @@ class HomePage { css: '.mm-banner-alert', }; + private readonly closeUseNetworkNotificationModalButton = { + text: 'Got it', + tag: 'h6', + }; + private readonly completedTransactions = '[data-testid="activity-list-item"]'; private readonly confirmedTransactions = { @@ -34,6 +39,8 @@ class HomePage { css: '.transaction-status-label--failed', }; + private readonly popoverBackground = '.popover-bg'; + private readonly sendButton = '[data-testid="eth-overview-send"]'; private readonly tokensTab = '[data-testid="account-overview__asset-tab"]'; @@ -60,8 +67,16 @@ class HomePage { console.log('Home page is loaded'); } - async startSendFlow(): Promise { - await this.driver.clickElement(this.sendButton); + async closeUseNetworkNotificationModal(): Promise { + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#25788) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await this.driver.assertElementNotPresent(this.popoverBackground); + await this.driver.clickElementSafe( + this.closeUseNetworkNotificationModalButton, + ); + await this.driver.assertElementNotPresent( + this.closeUseNetworkNotificationModalButton, + ); } async goToActivityList(): Promise { @@ -69,13 +84,6 @@ 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); @@ -85,6 +93,10 @@ class HomePage { await this.driver.clickElement(this.nftIconOnActivityList); } + async startSendFlow(): Promise { + await this.driver.clickElement(this.sendButton); + } + /** * Checks if the toaster message for adding a network is displayed on the homepage. * @@ -100,6 +112,37 @@ class HomePage { }); } + async check_basicFunctionalityOffWarnigMessageIsDisplayed(): Promise { + console.log( + 'Check if basic functionality off warning message is displayed on homepage', + ); + await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); + } + + /** + * This function checks the specified number of completed transactions are displayed in the activity list on the homepage. + * It waits up to 10 seconds for the expected number of completed transactions to be visible. + * + * @param expectedNumber - The number of completed transactions expected to be displayed in the activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of completed transactions is displayed within the timeout period. + */ + async check_completedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise { + console.log( + `Wait for ${expectedNumber} completed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const completedTxs = await this.driver.findElements( + this.completedTransactions, + ); + return completedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} completed transactions found in activity list on homepage`, + ); + } + /** * This function checks if the specified number of confirmed transactions are displayed in the activity list on homepage. * It waits up to 10 seconds for the expected number of confirmed transactions to be visible. @@ -125,27 +168,20 @@ class HomePage { } /** - * This function checks the specified number of completed transactions are displayed in the activity list on the homepage. - * It waits up to 10 seconds for the expected number of completed transactions to be visible. + * Checks if the toaster message for editing a network is displayed on the homepage. * - * @param expectedNumber - The number of completed transactions expected to be displayed in the activity list. Defaults to 1. - * @returns A promise that resolves if the expected number of completed transactions is displayed within the timeout period. + * @param networkName - The name of the network that was edited. */ - async check_completedTxNumberDisplayedInActivity( - expectedNumber: number = 1, + async check_editNetworkMessageIsDisplayed( + networkName: string, ): Promise { console.log( - `Wait for ${expectedNumber} completed transactions to be displayed in activity list`, - ); - await this.driver.wait(async () => { - const completedTxs = await this.driver.findElements( - this.completedTransactions, - ); - return completedTxs.length === expectedNumber; - }, 10000); - console.log( - `${expectedNumber} completed transactions found in activity list on homepage`, + `Check the toaster message for editing network ${networkName} is displayed on homepage`, ); + await this.driver.waitForSelector({ + tag: 'h6', + text: `“${networkName}” was successfully edited!`, + }); } /** 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 index dac2ab447710..e8288edb98cb 100644 --- a/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts +++ b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts @@ -134,8 +134,7 @@ class OnboardingPrivacySettingsPage { this.confirmAddCustomNetworkButton, ); // Navigate back to default privacy settings - await this.driver.clickElement(this.categoryBackButton); - await this.driver.waitForElementToStopMoving(this.categoryBackButton); + await this.navigateBackToSettingsPage(); } /** @@ -152,6 +151,16 @@ class OnboardingPrivacySettingsPage { ); } + /** + * Navigate back to the onboarding privacy settings page. + */ + async navigateBackToSettingsPage(): Promise { + console.log('Navigate back to onboarding privacy settings page'); + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + async navigateToGeneralSettings(): Promise { console.log('Navigate to general settings'); await this.check_pageIsLoaded(); @@ -159,6 +168,17 @@ class OnboardingPrivacySettingsPage { await this.driver.waitForSelector(this.generalSettingsMessage); } + /** + * Open the edit network modal for a given network name. + * + * @param networkName - The name of the network to open the edit modal for. + */ + async openEditNetworkModal(networkName: string): Promise { + console.log(`Open edit network modal for ${networkName}`); + await this.driver.clickElement({ text: networkName, tag: 'p' }); + await this.driver.waitForSelector(this.addRpcUrlDropDown); + } + /** * Go to assets settings and toggle options, then navigate back. */ @@ -172,8 +192,7 @@ class OnboardingPrivacySettingsPage { await this.driver.findClickableElements(this.assetsPrivacyToggle) ).map((toggle) => toggle.click()), ); - await this.driver.clickElement(this.categoryBackButton); - await this.driver.waitForElementToStopMoving(this.categoryBackButton); + await this.navigateBackToSettingsPage(); } /** @@ -186,8 +205,7 @@ class OnboardingPrivacySettingsPage { 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); + await this.navigateBackToSettingsPage(); } } diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 9da41bcb22d5..afff2f37e57e 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -34,6 +34,8 @@ class TestDapp { tag: 'button', }; + private readonly simpleSendButton = '#sendButton'; + private readonly erc721MintButton = '#mintButton'; private readonly erc721TransferFromButton = '#transferFromButton'; @@ -186,6 +188,10 @@ class TestDapp { }); } + async clickSimpleSendButton() { + await this.driver.clickElement(this.simpleSendButton); + } + async clickERC721MintButton() { await this.driver.clickElement(this.erc721MintButton); } diff --git a/test/e2e/tests/bridge/constants.ts b/test/e2e/tests/bridge/constants.ts index 924e5eb2b720..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/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/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/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/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 d7c2caead3a5..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", @@ -257,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/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index 6fc7025f5dbc..362a4c3e29e4 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -1,20 +1,27 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; import FixtureBuilder from '../../fixture-builder'; -import { - defaultGanacheOptions, - importSRPOnboardingFlow, - regularDelayMs, - TEST_SEED_PHRASE, - unlockWallet, - withFixtures, -} from '../../helpers'; +import { defaultGanacheOptions, withFixtures } from '../../helpers'; import { Driver } from '../../webdriver/driver'; import { Mockttp } from '../../mock-e2e'; import { expectMockRequest, expectNoMockRequest, } from '../../helpers/mock-server'; +import EditNetworkModal from '../../page-objects/pages/dialog/edit-network'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingPrivacySettingsPage from '../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import SelectNetwork from '../../page-objects/pages/dialog/select-network'; +import { + loginWithoutBalanceValidation, + loginWithBalanceValidation, +} from '../../page-objects/flows/login.flow'; +import { + completeImportSRPOnboardingFlow, + importSRPOnboardingFlow, +} from '../../page-objects/flows/onboarding.flow'; describe('MultiRpc:', function (this: Suite) { it('should migrate to multi rpc @no-mmi', async function () { @@ -73,36 +80,18 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver }: { driver: Driver }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); + await completeImportSRPOnboardingFlow(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); - await driver.delay(regularDelayMs); - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); - - // pin extension walkthrough screen - await driver.findElement('[data-testid="account-menu-icon"]'); - - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - await driver.clickElement( - '[data-testid="network-rpc-name-button-0xa4b1"]', - ); - - const menuItems = await driver.findElements('.select-rpc-url__item'); + await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); // check rpc number - assert.equal(menuItems.length, 2); + await selectNetworkDialog.openNetworkRPC('0xa4b1'); + await selectNetworkDialog.check_networkRPCNumber(2); }, ); }); @@ -173,7 +162,9 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver, mockedEndpoint }) => { - await unlockWallet(driver); + await loginWithoutBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); const usedUrlBeforeSwitch = await mockedEndpoint[1].getSeenRequests(); @@ -189,28 +180,21 @@ describe('MultiRpc:', function (this: Suite) { // check that requests are sent on the background for the rpc https://responsive-rpc.test/ await expectNoMockRequest(driver, mockedEndpoint[0], { timeout: 3000 }); - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - // select second rpc - await driver.clickElement( - '[data-testid="network-rpc-name-button-0xa4b1"]', - ); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.openNetworkRPC('0xa4b1'); + await selectNetworkDialog.check_networkRPCNumber(2); - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); + // select second rpc for Arbitrum network in the network dialog + await selectNetworkDialog.selectRPC('Arbitrum mainnet 2'); + await homePage.check_pageIsLoaded(); + await headerNavbar.clickSwitchNetworkDropDown(); - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + // check that the second rpc is selected in the network dialog + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); const usedUrl = await mockedEndpoint[0].getSeenRequests(); // check the url first request send on the background to the mocked rpc after switch @@ -218,9 +202,6 @@ describe('MultiRpc:', function (this: Suite) { // check that requests are sent on the background for the url https://responsive-rpc.test/ await expectMockRequest(driver, mockedEndpoint[0], { timeout: 3000 }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); }, ); }); @@ -280,53 +261,33 @@ describe('MultiRpc:', function (this: Suite) { testSpecificMock: mockRPCURLAndChainId, }, - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - // Go to Edit Menu - await driver.clickElement( - '[data-testid="network-list-item-options-button-0xa4b1"]', - ); - await driver.clickElement( - '[data-testid="network-list-item-options-edit"]', + async ({ driver, ganacheServer }) => { + await loginWithBalanceValidation(driver, ganacheServer); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + + // go to Edit Menu for Arbitrum network and select the second rpc + await selectNetworkDialog.openNetworkListOptions('0xa4b1'); + await selectNetworkDialog.openEditNetworkModal(); + + const editNetworkModal = new EditNetworkModal(driver); + await editNetworkModal.check_pageIsLoaded(); + await editNetworkModal.selectRPCInEditNetworkModal( + 'Arbitrum mainnet 2', ); - await driver.clickElement('[data-testid="test-add-rpc-drop-down"]'); - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + // validate the network was successfully edited + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_editNetworkMessageIsDisplayed('Arbitrum One'); + await homePage.closeUseNetworkNotificationModal(); - await driver.clickElement({ - text: 'Save', - tag: 'button', - }); - - // Validate the network was edited - const networkEdited = await driver.isElementPresent({ - text: '“Arbitrum One” was successfully edited!', - }); - assert.equal( - networkEdited, - true, - '“Arbitrum One” was successfully edited!', - ); - - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); + // check that the second rpc is selected in the network dialog + await headerNavbar.clickSwitchNetworkDropDown(); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); }, ); }); @@ -387,93 +348,41 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver }: { driver: Driver }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); - - await driver.delay(regularDelayMs); - - // go to advanced settigns - await driver.clickElementAndWaitToDisappear({ - text: 'Manage default privacy settings', - }); - - await driver.clickElement({ - text: 'General', - }); - - // open edit modal - await driver.clickElement({ - text: 'arbitrum-mainnet.infura.io', - tag: 'p', - }); - - await driver.clickElement('[data-testid="test-add-rpc-drop-down"]'); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - text: 'Save', - tag: 'button', - }); - - await driver.clickElement('[data-testid="category-back-button"]'); - - await driver.clickElement( - '[data-testid="privacy-settings-back-button"]', + await importSRPOnboardingFlow(driver); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, ); + await onboardingPrivacySettingsPage.check_pageIsLoaded(); + await onboardingPrivacySettingsPage.navigateToGeneralSettings(); - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - // Validate the network was edited - const networkEdited = await driver.isElementPresent({ - text: '“Arbitrum One” was successfully edited!', - }); - assert.equal( - networkEdited, - true, - '“Arbitrum One” was successfully edited!', + // open edit network modal during onboarding and select the second rpc + await onboardingPrivacySettingsPage.openEditNetworkModal( + 'Arbitrum One', ); - // Ensures popover backround doesn't kill test - await driver.assertElementNotPresent('.popover-bg'); - - // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) - // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed - await driver.clickElementSafe({ tag: 'h6', text: 'Got it' }); - - await driver.assertElementNotPresent({ - tag: 'h6', - text: 'Got it', - }); - - await driver.clickElement('[data-testid="network-display"]'); - - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); + const editNetworkModal = new EditNetworkModal(driver); + await editNetworkModal.check_pageIsLoaded(); + await editNetworkModal.selectRPCInEditNetworkModal( + 'Arbitrum mainnet 2', + ); + await onboardingPrivacySettingsPage.navigateBackToSettingsPage(); + await onboardingPrivacySettingsPage.check_pageIsLoaded(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + // finish onboarding and check the network successfully edited message is displayed + await onboardingCompletePage.completeOnboarding(); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_editNetworkMessageIsDisplayed('Arbitrum One'); + await homePage.closeUseNetworkNotificationModal(); + + // check that the second rpc is selected in the network dialog + await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); }, ); }); diff --git a/test/e2e/tests/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 01d90da9324c..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 @@ -6,154 +6,57 @@ const { unlockWallet, withFixtures, } = require('../../helpers'); +const { SECURITY_ALERTS_PROD_API_BASE_URL } = require('./constants'); const { mockServerJsonRpc } = require('./mocks/mock-server-json-rpc'); -const selectedAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; -const selectedAddressWithoutPrefix = '5cfe73b6021e818b776b421b1c4db2474086a7e1'; +const SELECTED_ADDRESS = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; -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 () { @@ -173,7 +76,7 @@ describe('PPOM Blockaid Alert - Malicious ERC20 Transfer @no-mmi', function () { }) .build(), defaultGanacheOptions, - testSpecificMock: mockInfura, + testSpecificMock: mockInfuraWithMaliciousResponses, title: this.test.fullTitle(), }, 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 e4ce73bcb615..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 @@ -3,11 +3,12 @@ const FixtureBuilder = require('../../fixture-builder'); const { defaultGanacheOptions, - logInWithBalanceValidation, + withFixtures, sendScreenToConfirmScreen, + logInWithBalanceValidation, WINDOW_TITLES, - 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"]'; @@ -18,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'], @@ -32,85 +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 .forGet( 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json', ) .thenCallback(() => { + console.log('mocked ppom_version.json'); return { statusCode: 500, }; @@ -144,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.`); }, 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/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/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/settings/localization.spec.js b/test/e2e/tests/settings/localization.spec.js index 1fb1e8d1e8a6..229c385efbeb 100644 --- a/test/e2e/tests/settings/localization.spec.js +++ b/test/e2e/tests/settings/localization.spec.js @@ -7,14 +7,16 @@ const FixtureBuilder = require('../../fixture-builder'); async function mockPhpConversion(mockServer) { return await mockServer - .forGet('https://min-api.cryptocompare.com/data/price') - .withQuery({ fsym: 'ETH', tsyms: 'PHP,USD' }) + .forGet('https://min-api.cryptocompare.com/data/pricemulti') + .withQuery({ fsyms: 'ETH', tsyms: 'php,USD' }) .thenCallback(() => { return { statusCode: 200, json: { - PHP: '100000', - USD: '2500', + ETH: { + PHP: '100000', + USD: '2500', + }, }, }; }); diff --git a/test/e2e/tests/simulation-details/mock-request-no-changes.ts b/test/e2e/tests/simulation-details/mock-request-no-changes.ts index 59b7fc9b8b3d..03aa562fd354 100644 --- a/test/e2e/tests/simulation-details/mock-request-no-changes.ts +++ b/test/e2e/tests/simulation-details/mock-request-no-changes.ts @@ -5,7 +5,7 @@ export const NO_CHANGES_TRANSACTION_MOCK = { maxFeePerGas: '0x0', maxPriorityFeePerGas: '0x0', to: SENDER_ADDRESS_MOCK, - value: '0x38d7ea4c68000', + value: '0x0', }; export const NO_CHANGES_REQUEST_MOCK: MockRequestResponse = { @@ -42,6 +42,7 @@ export const NO_CHANGES_REQUEST_MOCK: MockRequestResponse = { stateDiff: { post: { [SENDER_ADDRESS_MOCK]: { + balance: '0x3185e67a46d9066', nonce: '0x3c0', }, }, diff --git a/test/integration/config/setupAfter.js b/test/integration/config/setupAfter.js index 39eba1e429a5..ad9e49178094 100644 --- a/test/integration/config/setupAfter.js +++ b/test/integration/config/setupAfter.js @@ -1,2 +1,9 @@ // This file is for Jest-specific setup only and runs before our Jest tests. +import { jestPreviewConfigure } from 'jest-preview'; +import '../config/assets/index.css'; import '../../helpers/setup-after-helper'; + +// Should be path from root of your project +jestPreviewConfigure({ + publicFolder: 'test/integration/config/assets', // No need to configure if `publicFolder` is `public` +}); diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 8e9c979562f2..5ff87bf7c533 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -1,6 +1,7 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import nock from 'nock'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -42,6 +43,7 @@ const getMetaMaskStateWithUnapprovedPermitSign = (accountAddress: string) => { unapprovedTypedMessages: { [pendingPermitId]: { id: pendingPermitId, + chainId: CHAIN_IDS.SEPOLIA, status: 'unapproved', time: pendingPermitTime, type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 690446caa533..5a9c311c9abd 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,5 +1,6 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -34,6 +35,7 @@ const getMetaMaskStateWithUnapprovedPersonalSign = (accountAddress: string) => { unapprovedPersonalMsgs: { [pendingPersonalSignId]: { id: pendingPersonalSignId, + chainId: CHAIN_IDS.SEPOLIA, status: 'unapproved', time: pendingPersonalSignTime, type: MESSAGE_TYPE.PERSONAL_SIGN, diff --git a/test/integration/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/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..3a042de1ebb8 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { getTokenList } from '../../../../selectors'; +import { getTokenList, getPreferences } from '../../../../selectors'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; import { TokenListItem } from '../../../multichain'; import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; @@ -23,6 +23,7 @@ export default function TokenCell({ onClick, }: TokenCellProps) { const tokenList = useSelector(getTokenList); + const { privacyMode } = useSelector(getPreferences); const tokenData = Object.values(tokenList).find( (token) => isEqualCaseInsensitive(token.symbol, symbol) && @@ -51,6 +52,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..11190c68f267 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -30,7 +30,7 @@ export default function TokenList({ nativeToken, }: TokenListProps) { const t = useI18nContext(); - const { tokenSortConfig } = useSelector(getPreferences); + const { tokenSortConfig, tokenNetworkFilter } = useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); const nativeTokenWithBalance = useNativeTokenBalance(); @@ -52,6 +52,7 @@ export default function TokenList({ }; const sortedTokens = useMemo(() => { + // TODO filter assets by networkTokenFilter before sorting return sortAssets( [nativeTokenWithBalance, ...tokensWithBalances], tokenSortConfig, @@ -59,6 +60,7 @@ export default function TokenList({ }, [ tokensWithBalances, tokenSortConfig, + tokenNetworkFilter, conversionRate, contractExchangeRates, ]); 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__/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/constants.ts b/ui/components/app/confirm/info/row/constants.ts index f260f9bce282..415358aa5252 100644 --- a/ui/components/app/confirm/info/row/constants.ts +++ b/ui/components/app/confirm/info/row/constants.ts @@ -3,8 +3,9 @@ export const TEST_ADDRESS = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; export enum RowAlertKey { EstimatedFee = 'estimatedFee', SigningInWith = 'signingInWith', - Speed = 'speed', RequestFrom = 'requestFrom', + Resimulation = 'resimulation', + Speed = 'speed', } export enum AlertActionKey { 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/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index 290e737c0808..9545580bebbb 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -111,7 +111,7 @@ describe('SelectedAccount Component', () => { 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/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index 35f1af2ad414..c75b172a616e 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -13,6 +13,7 @@ import { import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; import { SnapUIAvatar } from '../snap-ui-avatar'; +import { useDisplayName } from '../../../../hooks/snaps/useDisplayName'; export type SnapUIAddressProps = { // The address must be a CAIP-10 string. @@ -40,12 +41,15 @@ export const SnapUIAddress: React.FunctionComponent = ({ [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 ( = ({ gap={2} > - {shortenedAddress} + {value} ); }; 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/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..9f267c96a53d 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,7 @@ export const CoinOverview = ({ const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover); const isTestnet = useSelector(getIsTestnet); - const { showFiatInTestnets } = useSelector(getPreferences); + const { showFiatInTestnets, privacyMode } = useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( @@ -163,6 +167,10 @@ export const CoinOverview = ({ dispatch(setAggregatedBalancePopoverShown()); }; + const handleSensitiveToggle = () => { + dispatch(setPrivacyMode(!privacyMode)); + }; + const [referenceElement, setReferenceElement] = useState(null); const setBoxRef = (ref: HTMLSpanElement | null) => { @@ -253,26 +261,38 @@ 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 4759af1ffa8c..47dc40200e69 100644 --- a/ui/components/app/wallet-overview/index.scss +++ b/ui/components/app/wallet-overview/index.scss @@ -9,6 +9,10 @@ flex-direction: column; width: 100%; + &-fullscreen { + align-items: center; + } + &__balance { flex: 1; display: flex; @@ -16,6 +20,10 @@ flex-direction: column; align-items: start; width: 100%; + + .wallet-overview-fullscreen > & { + align-items: center; + } } &__icon_button { @@ -70,7 +78,8 @@ display: flex; max-width: inherit; justify-content: center; - flex-wrap: wrap; + align-items: center; + flex-wrap: nowrap; } &__primary-balance { @@ -134,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/app/wallet-overview/wallet-overview.js b/ui/components/app/wallet-overview/wallet-overview.js index 213a7b2f2317..04127276acaf 100644 --- a/ui/components/app/wallet-overview/wallet-overview.js +++ b/ui/components/app/wallet-overview/wallet-overview.js @@ -2,9 +2,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +// TODO: Move this function to shared +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; + const WalletOverview = ({ balance, buttons, className }) => { return ( -
+
{balance}
{buttons}
diff --git a/ui/components/component-library/README.md b/ui/components/component-library/README.md index ec5006d3491f..a1f865bfe95a 100644 --- a/ui/components/component-library/README.md +++ b/ui/components/component-library/README.md @@ -4,7 +4,7 @@ This folder contains design system components that are built 1:1 with the Figma ## Architecture -All components are built on top of the `Box` component and accept all `Box` [component props](/docs/components-componentlibrary-box--docs#props). +All components are built on top of the `Box` component and accept all `Box` component props. ### Layout diff --git a/ui/components/component-library/sensitive-text/sensitive-text.types.ts b/ui/components/component-library/sensitive-text/sensitive-text.types.ts index 3834190df864..1ea8270d377f 100644 --- a/ui/components/component-library/sensitive-text/sensitive-text.types.ts +++ b/ui/components/component-library/sensitive-text/sensitive-text.types.ts @@ -30,7 +30,6 @@ export type SensitiveTextProps = Omit< * @default false */ isHidden?: boolean; - /** * Determines the length of the hidden text (number of asterisks). * Can be a predefined SensitiveTextLength or a custom string number. diff --git a/ui/components/component-library/text/README.mdx b/ui/components/component-library/text/README.mdx index 5b275aa48e23..183fcbbca9f8 100644 --- a/ui/components/component-library/text/README.mdx +++ b/ui/components/component-library/text/README.mdx @@ -580,7 +580,7 @@ Values using the `TextAlign` object from `./ui/helpers/constants/design-system.j ### Box Props -Box props are now integrated with the `Text` component. Valid Box props: [Box](/docs/components-componentlibrary-box--docs#props) +Box props are now integrated with the `Text` component. You no longer need to pass these props as an object through `boxProps` diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx index 06fe1336ca7e..e76dabc7add7 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx @@ -4,7 +4,7 @@ import { ICustodianType } from '@metamask-institutional/types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { hideModal } from '../../../store/actions'; -import { getSelectedInternalAccount } from '../../../selectors/selectors'; +import { getSelectedInternalAccount } from '../../../selectors/accounts'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { Box, diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx index 10dc049b8678..7c1d0f60488f 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx @@ -56,6 +56,7 @@ const InteractiveReplacementTokenNotification: React.FC< interactiveReplacementToken && Boolean(Object.keys(interactiveReplacementToken).length); + // @ts-expect-error keyring type is wrong maybe? if (!/^Custody/u.test(keyring.type) || !hasInteractiveReplacementToken) { setShowNotification(false); return; @@ -66,6 +67,7 @@ const InteractiveReplacementTokenNotification: React.FC< )) as unknown as string; const custodyAccountDetails = await dispatch( mmiActions.getAllCustodianAccountsWithToken( + // @ts-expect-error keyring type is wrong maybe? keyring.type.split(' - ')[1], token, ), @@ -105,6 +107,7 @@ const InteractiveReplacementTokenNotification: React.FC< interactiveReplacementToken?.oldRefreshToken, isUnlocked, dispatch, + // @ts-expect-error keyring type is wrong maybe? keyring.type, interactiveReplacementToken, mmiActions, diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap index 51f6f2e905f9..e320bd1de0e3 100644 --- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -242,6 +242,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > $100,000.00 @@ -538,6 +539,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > 0.006 @@ -581,6 +583,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > 0.006 diff --git a/ui/components/multichain/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 = ({ 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 9f77238bda9a..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 ( <> = ({ { setShowEditNetworksModal(true); 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/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..a0bb114409f6 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -1,9 +1,11 @@ import React from 'react'; +import { useSelector } from 'react-redux'; 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 { getPreferences } from '../../../selectors'; +import { SensitiveText, Box } from '../../component-library'; import { AlignItems, Display, @@ -35,6 +37,7 @@ export default function CurrencyDisplay({ isAggregatedFiatOverviewBalance = false, ...props }) { + const { privacyMode } = useSelector(getPreferences); const [title, parts] = useCurrencyDisplay(value, { account, displayValue, @@ -68,26 +71,33 @@ export default function CurrencyDisplay({ {prefixComponent}
) : null} - {parts.prefix} {parts.value} - + {parts.suffix ? ( - {parts.suffix} - + ) : null}
); diff --git a/ui/contexts/assetPolling.tsx b/ui/contexts/assetPolling.tsx new file mode 100644 index 000000000000..63cef9667fbd --- /dev/null +++ b/ui/contexts/assetPolling.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from 'react'; +import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; +import useTokenRatesPolling from '../hooks/useTokenRatesPolling'; + +// This provider is a step towards making controller polling fully UI based. +// Eventually, individual UI components will call the use*Polling hooks to +// poll and return particular data. This polls globally in the meantime. +export const AssetPollingProvider = ({ children }: { children: ReactNode }) => { + useCurrencyRatePolling(); + useTokenRatesPolling(); + + return <>{children}; +}; diff --git a/ui/contexts/currencyRate.js b/ui/contexts/currencyRate.js deleted file mode 100644 index 6739b730a882..000000000000 --- a/ui/contexts/currencyRate.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; - -export const CurrencyRateProvider = ({ children }) => { - useCurrencyRatePolling(); - - return <>{children}; -}; - -CurrencyRateProvider.propTypes = { - children: PropTypes.node, -}; diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 9a7a93ea958b..27b20a5841b3 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -339,4 +339,23 @@ describe('App State', () => { expect(state.showDataDeletionErrorModal).toStrictEqual(false); }); + + it('displays error in settings', () => { + const state = reduceApp(metamaskState, { + type: actions.SHOW_SETTINGS_PAGE_ERROR, + payload: 'settings page error', + }); + + expect(state.errorInSettings).toStrictEqual('settings page error'); + }); + + it('hides error in settings', () => { + const displayErrorInSettings = { errorInSettings: 'settings page error' }; + const oldState = { ...metamaskState, ...displayErrorInSettings }; + const state = reduceApp(oldState, { + type: actions.HIDE_SETTINGS_PAGE_ERROR, + }); + + expect(state.errorInSettings).toBeNull(); + }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index e6a7855ce7a5..81f875446f1e 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -105,6 +105,7 @@ type AppState = { snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; + errorInSettings: string | null; }; export type AppSliceState = { @@ -192,6 +193,7 @@ const initialState: AppState = { snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, + errorInSettings: null, }; export default function reduceApp( @@ -632,6 +634,16 @@ export default function reduceApp( ...appState, showDataDeletionErrorModal: false, }; + case actionConstants.SHOW_SETTINGS_PAGE_ERROR: + return { + ...appState, + errorInSettings: action.payload, + }; + case actionConstants.HIDE_SETTINGS_PAGE_ERROR: + return { + ...appState, + errorInSettings: null, + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -720,6 +732,27 @@ export function setCustomTokenAmount(payload: string): PayloadAction { return { type: actionConstants.SET_CUSTOM_TOKEN_AMOUNT, payload }; } +/** + * An action creator for display a error to the user in various places in the + * UI. It will not be cleared until a new warning replaces it or `hideWarning` + * is called. + * + * @param payload - The warning to show. + * @returns The action to display the warning. + */ +export function displayErrorInSettings(payload: string): PayloadAction { + return { + type: actionConstants.SHOW_SETTINGS_PAGE_ERROR, + payload, + }; +} + +export function hideErrorInSettings() { + return { + type: actionConstants.HIDE_SETTINGS_PAGE_ERROR, + }; +} + // Selectors export function getQrCodeData(state: AppSliceState): { type?: string | null; diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 5e50b004b774..c3854bdc4ae8 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -11,31 +11,31 @@ import { import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; +import { QuoteRequest } from '../../pages/bridge/types'; import { bridgeSlice } from './bridge'; const { - setToChainId: setToChainId_, + setToChainId, setFromToken, setToToken, setFromTokenInputValue, resetInputFields, - switchToAndFromTokens, } = bridgeSlice.actions; export { - setFromToken, + setToChainId, + resetInputFields, setToToken, + setFromToken, setFromTokenInputValue, - switchToAndFromTokens, - resetInputFields, }; const callBridgeControllerMethod = ( bridgeAction: BridgeUserAction | BridgeBackgroundAction, - args?: T[], + args?: T, ) => { return async (dispatch: MetaMaskReduxDispatch) => { - await submitRequestToBackground(bridgeAction, args); + await submitRequestToBackground(bridgeAction, [args]); await forceUpdateMetamaskState(dispatch); }; }; @@ -53,20 +53,29 @@ export const setBridgeFeatureFlags = () => { export const setFromChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { dispatch( - callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [ + callBridgeControllerMethod( + BridgeUserAction.SELECT_SRC_NETWORK, chainId, - ]), + ), ); }; }; export const setToChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { - dispatch(setToChainId_(chainId)); dispatch( - callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [ + callBridgeControllerMethod( + BridgeUserAction.SELECT_DEST_NETWORK, chainId, - ]), + ), + ); + }; +}; + +export const updateQuoteRequestParams = (params: Partial) => { + return async (dispatch: MetaMaskReduxDispatch) => { + await dispatch( + callBridgeControllerMethod(BridgeUserAction.UPDATE_QUOTE_PARAMS, params), ); }; }; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index f4a566c233b5..6b85565c6143 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -1,5 +1,6 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { zeroAddress } from 'ethereumjs-util'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { setBackgroundConnection } from '../../store/background-connection'; @@ -18,7 +19,8 @@ import { setToToken, setFromChain, resetInputFields, - switchToAndFromTokens, + setToChainId, + updateQuoteRequestParams, } from './actions'; const middleware = [thunk]; @@ -31,11 +33,25 @@ describe('Ducks - Bridge', () => { store.clearActions(); }); - describe('setToChain', () => { - it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => { + describe('setToChainId', () => { + it('calls the "bridge/setToChainId" action', () => { const state = store.getState().bridge; const actionPayload = CHAIN_IDS.OPTIMISM; + store.dispatch(setToChainId(actionPayload as never) as never); + + // Check redux state + const actions = store.getActions(); + expect(actions[0].type).toStrictEqual('bridge/setToChainId'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toChainId).toStrictEqual(actionPayload); + }); + }); + + describe('setToChain', () => { + it('calls the selectDestNetwork background action', () => { + const actionPayload = CHAIN_IDS.OPTIMISM; + const mockSelectDestNetwork = jest.fn().mockReturnValue({}); setBackgroundConnection({ [BridgeUserAction.SELECT_DEST_NETWORK]: mockSelectDestNetwork, @@ -43,11 +59,6 @@ describe('Ducks - Bridge', () => { store.dispatch(setToChain(actionPayload as never) as never); - // Check redux state - const actions = store.getActions(); - expect(actions[0].type).toStrictEqual('bridge/setToChainId'); - const newState = bridgeReducer(state, actions[0]); - expect(newState.toChainId).toStrictEqual(actionPayload); // Check background state expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); expect(mockSelectDestNetwork).toHaveBeenCalledWith( @@ -61,7 +72,7 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; - store.dispatch(setFromToken(actionPayload)); + store.dispatch(setFromToken(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromToken'); const newState = bridgeReducer(state, actions[0]); @@ -73,7 +84,8 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setToToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; - store.dispatch(setToToken(actionPayload)); + + store.dispatch(setToToken(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setToToken'); const newState = bridgeReducer(state, actions[0]); @@ -85,7 +97,8 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromTokenInputValue" action', () => { const state = store.getState().bridge; const actionPayload = '10'; - store.dispatch(setFromTokenInputValue(actionPayload)); + + store.dispatch(setFromTokenInputValue(actionPayload as never) as never); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue'); const newState = bridgeReducer(state, actions[0]); @@ -137,31 +150,30 @@ describe('Ducks - Bridge', () => { }); }); - describe('switchToAndFromTokens', () => { - it('switches to and from input values', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bridgeStore = configureMockStore(middleware)( - createBridgeMockStore( - {}, - { - toChainId: CHAIN_IDS.MAINNET, - fromToken: { symbol: 'WETH', address: '0x13341432' }, - toToken: { symbol: 'USDC', address: '0x13341431' }, - fromTokenInputValue: '10', - }, - ), + describe('updateQuoteRequestParams', () => { + it('dispatches quote params to the bridge controller', () => { + const mockUpdateParams = jest.fn(); + setBackgroundConnection({ + [BridgeUserAction.UPDATE_QUOTE_PARAMS]: mockUpdateParams, + } as never); + + store.dispatch( + updateQuoteRequestParams({ + srcChainId: 1, + srcTokenAddress: zeroAddress(), + destTokenAddress: undefined, + }) as never, + ); + + expect(mockUpdateParams).toHaveBeenCalledTimes(1); + expect(mockUpdateParams).toHaveBeenCalledWith( + { + srcChainId: 1, + srcTokenAddress: zeroAddress(), + destTokenAddress: undefined, + }, + expect.anything(), ); - const state = bridgeStore.getState().bridge; - bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON)); - const actions = bridgeStore.getActions(); - expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens'); - const newState = bridgeReducer(state, actions[0]); - expect(newState).toStrictEqual({ - toChainId: CHAIN_IDS.POLYGON, - fromToken: { symbol: 'USDC', address: '0x13341431' }, - toToken: { symbol: 'WETH', address: '0x13341432' }, - fromTokenInputValue: null, - }); }); }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 9ec744d9e953..c75030c7591d 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -39,12 +39,6 @@ const bridgeSlice = createSlice({ resetInputFields: () => ({ ...initialState, }), - switchToAndFromTokens: (state, { payload }) => ({ - toChainId: payload, - fromToken: state.toToken, - toToken: state.fromToken, - fromTokenInputValue: null, - }), }, }); diff --git a/ui/ducks/bridge/selectors.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/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/snaps/useDisplayName.ts b/ui/hooks/snaps/useDisplayName.ts new file mode 100644 index 000000000000..6a6d3d7e6b51 --- /dev/null +++ b/ui/hooks/snaps/useDisplayName.ts @@ -0,0 +1,54 @@ +import { NamespaceId } from '@metamask/snaps-utils'; +import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; +import { useSelector } from 'react-redux'; +import { + getMemoizedAccountName, + getAddressBookEntryByNetwork, + AddressBookMetaMaskState, + AccountsMetaMaskState, +} from '../../selectors/snaps'; +import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; +import { decimalToHex } from '../../../shared/modules/conversion.utils'; + +export type UseDisplayNameParams = { + chain: { + namespace: NamespaceId; + reference: string; + }; + chainId: CaipChainId; + address: string; +}; + +/** + * Get the display name for an address. + * This will look for an account name in the state, and if not found, it will look for an address book entry. + * + * @param params - The parsed CAIP-10 ID. + * @returns The display name for the address. + */ +export const useDisplayName = ( + params: UseDisplayNameParams, +): string | undefined => { + const { + address, + chain: { namespace, reference }, + } = params; + + const isEip155 = namespace === KnownCaipNamespace.Eip155; + + const parsedAddress = isEip155 ? toChecksumHexAddress(address) : address; + + const accountName = useSelector((state: AccountsMetaMaskState) => + getMemoizedAccountName(state, parsedAddress), + ); + + const addressBookEntry = useSelector((state: AddressBookMetaMaskState) => + getAddressBookEntryByNetwork( + state, + parsedAddress, + `0x${decimalToHex(isEip155 ? reference : `0`)}`, + ), + ); + + return accountName || (isEip155 && addressBookEntry?.name) || undefined; +}; diff --git a/ui/hooks/useAccountTotalFiatBalance.js b/ui/hooks/useAccountTotalFiatBalance.js index b0c9b293c906..7b4a4675225a 100644 --- a/ui/hooks/useAccountTotalFiatBalance.js +++ b/ui/hooks/useAccountTotalFiatBalance.js @@ -51,7 +51,6 @@ export const useAccountTotalFiatBalance = ( const tokens = detectedTokens?.[currentChainId]?.[account?.address] ?? []; // This selector returns all the tokens, we need it to get the image of token const allTokenList = useSelector(getTokenList); - const allTokenListValues = Object.values(allTokenList); const primaryTokenImage = useSelector(getNativeCurrencyImage); const nativeCurrency = useSelector(getNativeCurrency); @@ -92,20 +91,18 @@ export const useAccountTotalFiatBalance = ( }; // To match the list of detected tokens with the entire token list to find the image for tokens - const findMatchingTokens = (array1, array2) => { + const findMatchingTokens = (tokenList, _tokensWithBalances) => { const result = []; - array2.forEach((token2) => { - const matchingToken = array1.find( - (token1) => token1.symbol === token2.symbol, - ); + _tokensWithBalances.forEach((token) => { + const matchingToken = tokenList[token.address.toLowerCase()]; if (matchingToken) { result.push({ ...matchingToken, - balance: token2.balance, - string: token2.string, - balanceError: token2.balanceError, + balance: token.balance, + string: token.string, + balanceError: token.balanceError, }); } }); @@ -113,10 +110,7 @@ export const useAccountTotalFiatBalance = ( return result; }; - const matchingTokens = findMatchingTokens( - allTokenListValues, - tokensWithBalances, - ); + const matchingTokens = findMatchingTokens(allTokenList, tokensWithBalances); // Combine native token, detected token with image in an array const allTokensWithFiatValues = [ diff --git a/ui/hooks/useCurrencyRatePolling.ts b/ui/hooks/useCurrencyRatePolling.ts index fb14b1c94797..e7ad21adedf5 100644 --- a/ui/hooks/useCurrencyRatePolling.ts +++ b/ui/hooks/useCurrencyRatePolling.ts @@ -1,24 +1,30 @@ import { useSelector } from 'react-redux'; import { - getSelectedNetworkClientId, + getNetworkConfigurationsByChainId, getUseCurrencyRateCheck, } from '../selectors'; import { - currencyRateStartPollingByNetworkClientId, + currencyRateStartPolling, currencyRateStopPollingByPollingToken, } from '../store/actions'; import { getCompletedOnboarding } from '../ducks/metamask/metamask'; import usePolling from './usePolling'; -const useCurrencyRatePolling = (networkClientId?: string) => { +const useCurrencyRatePolling = () => { const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const completedOnboarding = useSelector(getCompletedOnboarding); - const selectedNetworkClientId = useSelector(getSelectedNetworkClientId); + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + + const nativeCurrencies = [ + ...new Set( + Object.values(networkConfigurations).map((n) => n.nativeCurrency), + ), + ]; usePolling({ - startPollingByNetworkClientId: currencyRateStartPollingByNetworkClientId, + startPolling: currencyRateStartPolling, stopPollingByPollingToken: currencyRateStopPollingByPollingToken, - networkClientId: networkClientId ?? selectedNetworkClientId, + input: nativeCurrencies, enabled: useCurrencyRateCheck && completedOnboarding, }); }; diff --git a/ui/hooks/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/useMultiPolling.ts b/ui/hooks/useMultiPolling.ts new file mode 100644 index 000000000000..f0b3ed33cdfc --- /dev/null +++ b/ui/hooks/useMultiPolling.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; + +type UseMultiPollingOptions = { + startPolling: (input: PollingInput) => Promise; + stopPollingByPollingToken: (pollingToken: string) => void; + input: PollingInput[]; +}; + +// A hook that manages multiple polling loops of a polling controller. +// Callers provide an array of inputs, and the hook manages starting +// and stopping polling loops for each input. +const useMultiPolling = ( + usePollingOptions: UseMultiPollingOptions, +) => { + const [polls, setPolls] = useState(new Map()); + + useEffect(() => { + // start new polls + for (const input of usePollingOptions.input) { + const key = JSON.stringify(input); + if (!polls.has(key)) { + usePollingOptions + .startPolling(input) + .then((token) => + setPolls((prevPolls) => new Map(prevPolls).set(key, token)), + ); + } + } + + // stop existing polls + for (const [inputKey, token] of polls.entries()) { + const exists = usePollingOptions.input.some( + (i) => inputKey === JSON.stringify(i), + ); + + if (!exists) { + usePollingOptions.stopPollingByPollingToken(token); + setPolls((prevPolls) => { + const newPolls = new Map(prevPolls); + newPolls.delete(inputKey); + return newPolls; + }); + } + } + }, [usePollingOptions.input && JSON.stringify(usePollingOptions.input)]); + + // stop all polling on dismount + useEffect(() => { + return () => { + for (const token of polls.values()) { + usePollingOptions.stopPollingByPollingToken(token); + } + }; + }, []); +}; + +export default useMultiPolling; diff --git a/ui/hooks/useMultichainSelector.ts b/ui/hooks/useMultichainSelector.ts index 326ac79bf9cd..9bd979df7e7e 100644 --- a/ui/hooks/useMultichainSelector.ts +++ b/ui/hooks/useMultichainSelector.ts @@ -11,6 +11,7 @@ export function useMultichainSelector< ) { return useSelector((state: TState) => { // We either pass an account or fallback to the currently selected one + // @ts-expect-error state types don't match return selector(state, account || getSelectedInternalAccount(state)); }); } diff --git a/ui/hooks/useName.test.ts b/ui/hooks/useName.test.ts index f746c4bb6267..b102e9dce7a0 100644 --- a/ui/hooks/useName.test.ts +++ b/ui/hooks/useName.test.ts @@ -15,7 +15,6 @@ jest.mock('react-redux', () => ({ })); jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), getNames: jest.fn(), })); diff --git a/ui/hooks/useNftCollectionsMetadata.test.ts b/ui/hooks/useNftCollectionsMetadata.test.ts index e1e2b6745ad1..cf7997cb518b 100644 --- a/ui/hooks/useNftCollectionsMetadata.test.ts +++ b/ui/hooks/useNftCollectionsMetadata.test.ts @@ -16,10 +16,6 @@ jest.mock('react-redux', () => ({ useSelector: (selector: any) => selector(), })); -jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), -})); - jest.mock('../store/actions', () => ({ getNFTContractInfo: jest.fn(), getTokenStandardAndDetails: jest.fn(), diff --git a/ui/hooks/usePolling.test.js b/ui/hooks/usePolling.test.js index 9250257d3cbc..a556bb86be54 100644 --- a/ui/hooks/usePolling.test.js +++ b/ui/hooks/usePolling.test.js @@ -4,13 +4,12 @@ import usePolling from './usePolling'; describe('usePolling', () => { // eslint-disable-next-line jest/no-done-callback - it('calls startPollingByNetworkClientId and callback option args with polling token when component instantiating the hook mounts', (done) => { + it('calls startPolling and calls back with polling token when component instantiating the hook mounts', (done) => { const mockStart = jest.fn().mockImplementation(() => { return Promise.resolve('pollingToken'); }); const mockStop = jest.fn(); const networkClientId = 'mainnet'; - const options = {}; const mockState = { metamask: {}, }; @@ -18,17 +17,16 @@ describe('usePolling', () => { renderHookWithProvider(() => { usePolling({ callback: (pollingToken) => { - expect(mockStart).toHaveBeenCalledWith(networkClientId, options); + expect(mockStart).toHaveBeenCalledWith({ networkClientId }); expect(pollingToken).toBeDefined(); done(); return (_pollingToken) => { // noop }; }, - startPollingByNetworkClientId: mockStart, + startPolling: mockStart, stopPollingByPollingToken: mockStop, - networkClientId, - options, + input: { networkClientId }, }); }, mockState); }); @@ -39,7 +37,6 @@ describe('usePolling', () => { }); const mockStop = jest.fn(); const networkClientId = 'mainnet'; - const options = {}; const mockState = { metamask: {}, }; @@ -54,10 +51,9 @@ describe('usePolling', () => { done(); }; }, - startPollingByNetworkClientId: mockStart, + startPolling: mockStart, stopPollingByPollingToken: mockStop, - networkClientId, - options, + input: { networkClientId }, }), mockState, ); diff --git a/ui/hooks/usePolling.ts b/ui/hooks/usePolling.ts index 1a9d6b1f576e..613e70cf17b5 100644 --- a/ui/hooks/usePolling.ts +++ b/ui/hooks/usePolling.ts @@ -1,22 +1,16 @@ import { useEffect, useRef } from 'react'; -type UsePollingOptions = { +type UsePollingOptions = { callback?: (pollingToken: string) => (pollingToken: string) => void; - startPollingByNetworkClientId: ( - networkClientId: string, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options: any, - ) => Promise; + startPolling: (input: PollingInput) => Promise; stopPollingByPollingToken: (pollingToken: string) => void; - networkClientId: string; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options?: any; + input: PollingInput; enabled?: boolean; }; -const usePolling = (usePollingOptions: UsePollingOptions) => { +const usePolling = ( + usePollingOptions: UsePollingOptions, +) => { const pollTokenRef = useRef(null); const cleanupRef = useRef void)>(null); let isMounted = false; @@ -38,10 +32,7 @@ const usePolling = (usePollingOptions: UsePollingOptions) => { // Start polling when the component mounts usePollingOptions - .startPollingByNetworkClientId( - usePollingOptions.networkClientId, - usePollingOptions.options, - ) + .startPolling(usePollingOptions.input) .then((pollToken) => { pollTokenRef.current = pollToken; cleanupRef.current = usePollingOptions.callback?.(pollToken) || null; @@ -56,12 +47,7 @@ const usePolling = (usePollingOptions: UsePollingOptions) => { cleanup(); }; }, [ - usePollingOptions.networkClientId, - usePollingOptions.options && - JSON.stringify( - usePollingOptions.options, - Object.keys(usePollingOptions.options).sort(), - ), + usePollingOptions.input && JSON.stringify(usePollingOptions.input), usePollingOptions.enabled, ]); }; diff --git a/ui/hooks/useTokenRatesPolling.ts b/ui/hooks/useTokenRatesPolling.ts new file mode 100644 index 000000000000..41c1c8793b97 --- /dev/null +++ b/ui/hooks/useTokenRatesPolling.ts @@ -0,0 +1,40 @@ +import { useSelector } from 'react-redux'; +import { + getMarketData, + getNetworkConfigurationsByChainId, + getTokenExchangeRates, + getTokensMarketData, + getUseCurrencyRateCheck, +} from '../selectors'; +import { + tokenRatesStartPolling, + tokenRatesStopPollingByPollingToken, +} from '../store/actions'; +import useMultiPolling from './useMultiPolling'; + +const useTokenRatesPolling = ({ chainIds }: { chainIds?: string[] } = {}) => { + // Selectors to determine polling input + const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + + // Selectors returning state updated by the polling + const tokenExchangeRates = useSelector(getTokenExchangeRates); + const tokensMarketData = useSelector(getTokensMarketData); + const marketData = useSelector(getMarketData); + + useMultiPolling({ + startPolling: tokenRatesStartPolling, + stopPollingByPollingToken: tokenRatesStopPollingByPollingToken, + input: useCurrencyRateCheck + ? chainIds ?? Object.keys(networkConfigurations) + : [], + }); + + return { + tokenExchangeRates, + tokensMarketData, + marketData, + }; +}; + +export default useTokenRatesPolling; diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index 95828e3e250e..b5ebc0a83eb6 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -268,17 +268,17 @@ exports[`AssetPage should render a native asset 1`] = `

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

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

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

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

$1.00

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

$1.00

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

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

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

{ 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], @@ -39,6 +43,10 @@ describe('Bridge utils', () => { }); expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 1, + refreshRate: 3, + }, extensionSupport: true, srcNetworkAllowlist: [ CHAIN_IDS.MAINNET, @@ -78,6 +86,10 @@ describe('Bridge utils', () => { }); expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 5, + refreshRate: 30000, + }, extensionSupport: false, srcNetworkAllowlist: [], destNetworkAllowlist: [], diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index 4641d288979f..f154b7e62b19 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -23,6 +23,9 @@ 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, @@ -64,6 +67,8 @@ export async function fetchBridgeFeatureFlags(): Promise { ) ) { return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: + rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG], [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: rawFeatureFlags[BridgeFlag.EXTENSION_SUPPORT], [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: rawFeatureFlags[ @@ -76,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 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 index e8bf48904489..5d001e7ef7fc 100644 --- a/ui/pages/bridge/types.ts +++ b/ui/pages/bridge/types.ts @@ -7,6 +7,10 @@ export enum BridgeFlag { } export type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + }; [BridgeFlag.EXTENSION_SUPPORT]: boolean; [BridgeFlag.NETWORK_SRC_ALLOWLIST]: number[]; [BridgeFlag.NETWORK_DEST_ALLOWLIST]: number[]; diff --git a/ui/pages/bridge/utils/validators.ts b/ui/pages/bridge/utils/validators.ts index f0d3161c3b15..01c716522968 100644 --- a/ui/pages/bridge/utils/validators.ts +++ b/ui/pages/bridge/utils/validators.ts @@ -4,7 +4,7 @@ import { truthyDigitString, validateData, } from '../../../../shared/lib/swaps-utils'; -import { BridgeFlag } from '../types'; +import { BridgeFlag, FeatureFlagResponse } from '../types'; type Validator = { property: keyof ExpectedResponse | string; @@ -29,6 +29,18 @@ 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, diff --git a/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap b/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap index b13f1f6d31e9..e35c865829b8 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap +++ b/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap @@ -51,6 +51,7 @@ exports[`ConfirmGasDisplay should match snapshot 1`] = ` > 0.001197 @@ -113,6 +114,7 @@ exports[`ConfirmGasDisplay should match snapshot 1`] = ` > 0.00147 diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap index f6e40da8118c..db005f8c02e0 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/__snapshots__/confirm-legacy-gas-display.test.js.snap @@ -51,6 +51,7 @@ exports[`ConfirmLegacyGasDisplay should match snapshot 1`] = ` > 0.000021 @@ -67,6 +68,7 @@ exports[`ConfirmLegacyGasDisplay should match snapshot 1`] = ` > 0.000021 @@ -100,6 +102,7 @@ exports[`ConfirmLegacyGasDisplay should match snapshot 1`] = ` > 0.000021 diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/__snapshots__/confirm-detail-row.component.test.js.snap b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/__snapshots__/confirm-detail-row.component.test.js.snap index 8a3053f67d88..073d23aebf88 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/__snapshots__/confirm-detail-row.component.test.js.snap +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/__snapshots__/confirm-detail-row.component.test.js.snap @@ -27,6 +27,7 @@ exports[`Confirm Detail Row Component should match snapshot 1`] = `
0 @@ -47,6 +48,7 @@ exports[`Confirm Detail Row Component should match snapshot 1`] = `
0 diff --git a/ui/pages/confirmations/components/confirm/header/header.tsx b/ui/pages/confirmations/components/confirm/header/header.tsx index 6c7c0e1cdd7f..278b21f85cbb 100644 --- a/ui/pages/confirmations/components/confirm/header/header.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.tsx @@ -3,6 +3,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React from 'react'; +import { ORIGIN_METAMASK } from '../../../../../../shared/constants/app'; import { AvatarNetwork, AvatarNetworkSize, @@ -30,6 +31,7 @@ const CONFIRMATIONS_WITH_NEW_HEADER = [ TransactionType.tokenMethodTransfer, TransactionType.tokenMethodTransferFrom, TransactionType.tokenMethodSafeTransferFrom, + TransactionType.simpleSend, ]; const Header = () => { @@ -88,7 +90,7 @@ const Header = () => { currentConfirmation?.type && CONFIRMATIONS_WITH_NEW_HEADER.includes(currentConfirmation.type); const isWalletInitiated = - (currentConfirmation as TransactionMeta)?.origin === 'metamask'; + (currentConfirmation as TransactionMeta)?.origin === ORIGIN_METAMASK; if (isConfirmationWithNewHeader && isWalletInitiated) { return ; } else if (isConfirmationWithNewHeader && !isWalletInitiated) { diff --git a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap index e466d6b5e11e..38219749e987 100644 --- a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap @@ -178,26 +178,35 @@ exports[`Info renders info section for contract interaction request 1`] = ` class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" >
-

- Estimated changes -

-
- +

+ Estimated changes +

+
+
+ +
+
diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index 2762e99652a5..f908333e4f25 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -124,6 +124,8 @@ export const EditSpendingCapModal = ({ decimals && parseInt(decimals, 10) < countDecimalDigits(customSpendingCapInputValue); + const showSpecialCharacterError = /[-+e]/u.test(customSpendingCapInputValue); + return ( )} + {showSpecialCharacterError && ( + + {t('editSpendingCapSpecialCharError')} + + )} diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts index 4173d21910c5..0178e2ffff62 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.test.ts @@ -65,7 +65,7 @@ describe('useApproveTokenSimulation', () => { expect(result.current).toMatchInlineSnapshot(` { - "formattedSpendingCap": 7, + "formattedSpendingCap": "7", "pending": undefined, "spendingCap": "#7", "value": { @@ -155,4 +155,70 @@ describe('useApproveTokenSimulation', () => { } `); }); + + it('returns correct small decimal number token amount for fungible tokens', async () => { + const useIsNFTMock = jest.fn().mockImplementation(() => ({ isNFT: false })); + + const useDecodedTransactionDataMock = jest.fn().mockImplementation(() => ({ + pending: false, + value: { + data: [ + { + name: 'approve', + params: [ + { + type: 'address', + value: '0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', + }, + { + type: 'uint256', + value: 10 ** 5, + }, + ], + }, + ], + source: 'FourByte', + }, + })); + + (useIsNFT as jest.Mock).mockImplementation(useIsNFTMock); + (useDecodedTransactionData as jest.Mock).mockImplementation( + useDecodedTransactionDataMock, + ); + + const transactionMeta = genUnapprovedContractInteractionConfirmation({ + address: CONTRACT_INTERACTION_SENDER_ADDRESS, + }) as TransactionMeta; + + const { result } = renderHookWithProvider( + () => useApproveTokenSimulation(transactionMeta, '18'), + mockState, + ); + + expect(result.current).toMatchInlineSnapshot(` + { + "formattedSpendingCap": "0.0000000000001", + "pending": undefined, + "spendingCap": "0.0000000000001", + "value": { + "data": [ + { + "name": "approve", + "params": [ + { + "type": "address", + "value": "0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4", + }, + { + "type": "uint256", + "value": 100000, + }, + ], + }, + ], + "source": "FourByte", + }, + } + `); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts index 19f26c9c9300..ce264285bc13 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts @@ -6,6 +6,7 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getIntlLocale } from '../../../../../../../ducks/locale/locale'; import { SPENDING_CAP_UNLIMITED_MSG } from '../../../../../constants'; +import { toNonScientificString } from '../../hooks/use-token-values'; import { useDecodedTransactionData } from '../../hooks/useDecodedTransactionData'; import { useIsNFT } from './use-is-nft'; @@ -46,8 +47,9 @@ export const useApproveTokenSimulation = ( }, [value, decimals]); const formattedSpendingCap = useMemo(() => { - return isNFT - ? decodedSpendingCap + // formatting coerces small numbers to 0 + return isNFT || decodedSpendingCap < 1 + ? toNonScientificString(decodedSpendingCap) : new Intl.NumberFormat(locale).format(decodedSpendingCap); }, [decodedSpendingCap, isNFT, locale]); diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap index f88485e985b3..47d28a0ba29d 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap @@ -15,26 +15,35 @@ exports[` renders component for contract interaction requ class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" >
-

- Estimated changes -

-
- +

+ Estimated changes +

+
+
+ +
+
diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts index efdf2b66ac56..9011569bac3e 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts @@ -1,11 +1,39 @@ import { TransactionMeta } from '@metamask/transaction-controller'; +import { useSelector } from 'react-redux'; import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; import mockState from '../../../../../../../test/data/mock-state.json'; import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; +import { getTokenList } from '../../../../../../selectors'; import { useTokenDetails } from './useTokenDetails'; +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +const ICON_SYMBOL = 'FROG'; +const ICON_URL = + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a2c375553e6965b42c135bb8b15a8914b08de0c.png'; +const MOCK_TOKEN_LIST = (transactionMeta: TransactionMeta) => ({ + [transactionMeta.txParams.to as string]: { + address: transactionMeta.txParams.to, + aggregators: ['CoinGecko', 'Socket', 'Coinmarketcap'], + decimals: 9, + iconUrl: ICON_URL, + name: 'Frog on ETH', + occurrences: 3, + symbol: ICON_SYMBOL, + }, +}); + describe('useTokenDetails', () => { - it('returns iconUrl from selected token if it exists', () => { + const useSelectorMock = useSelector as jest.Mock; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns token details from selected token if it exists', () => { const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, ) as TransactionMeta; @@ -18,8 +46,17 @@ describe('useTokenDetails', () => { image: 'image', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta), mockState, ); @@ -29,6 +66,37 @@ describe('useTokenDetails', () => { }); }); + it('returns token details from the token list if it exists', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + }; + + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + + return undefined; + }); + + const { result } = renderHookWithProvider( + () => useTokenDetails(transactionMeta), + mockState, + ); + + expect(result.current).toEqual({ + tokenImage: ICON_URL, + tokenSymbol: ICON_SYMBOL, + }); + }); + it('returns selected token image if no iconUrl is included', () => { const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, @@ -41,8 +109,17 @@ describe('useTokenDetails', () => { image: 'image', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta), mockState, ); @@ -63,23 +140,22 @@ describe('useTokenDetails', () => { symbol: 'symbol', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), - { - ...mockState, - metamask: { - ...mockState.metamask, - tokenList: { - '0x076146c765189d51be3160a2140cf80bfc73ad68': { - iconUrl: 'tokenListIconUrl', - }, - }, - }, - }, + () => useTokenDetails(transactionMeta), + mockState, ); expect(result.current).toEqual({ - tokenImage: 'tokenListIconUrl', + tokenImage: ICON_URL, tokenSymbol: 'symbol', }); }); @@ -95,8 +171,17 @@ describe('useTokenDetails', () => { symbol: 'symbol', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return {}; + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta), mockState, ); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts index be9578496205..da2faacaeb5f 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts @@ -2,15 +2,14 @@ import { TokenListMap } from '@metamask/assets-controllers'; import { TransactionMeta } from '@metamask/transaction-controller'; import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; -import { getTokenList } from '../../../../../../selectors'; -import { SelectedToken } from '../shared/selected-token'; +import { getTokenList, getWatchedToken } from '../../../../../../selectors'; +import { MultichainState } from '../../../../../../selectors/multichain'; -export const useTokenDetails = ( - transactionMeta: TransactionMeta, - selectedToken: SelectedToken, -) => { +export const useTokenDetails = (transactionMeta: TransactionMeta) => { const t = useI18nContext(); - + const selectedToken = useSelector((state: MultichainState) => + getWatchedToken(transactionMeta)(state), + ); const tokenList = useSelector(getTokenList) as TokenListMap; const tokenImage = diff --git a/ui/pages/confirmations/components/confirm/info/info.tsx b/ui/pages/confirmations/components/confirm/info/info.tsx index 67bac40d7e61..f283cbfc2e61 100644 --- a/ui/pages/confirmations/components/confirm/info/info.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.tsx @@ -4,19 +4,23 @@ import { useConfirmContext } from '../../../context/confirm'; import { SignatureRequestType } from '../../../types/confirm'; import ApproveInfo from './approve/approve'; import BaseTransactionInfo from './base-transaction-info/base-transaction-info'; +import NativeTransferInfo from './native-transfer/native-transfer'; +import NFTTokenTransferInfo from './nft-token-transfer/nft-token-transfer'; import PersonalSignInfo from './personal-sign/personal-sign'; import SetApprovalForAllInfo from './set-approval-for-all-info/set-approval-for-all-info'; import TokenTransferInfo from './token-transfer/token-transfer'; import TypedSignV1Info from './typed-sign-v1/typed-sign-v1'; import TypedSignInfo from './typed-sign/typed-sign'; -import NFTTokenTransferInfo from './nft-token-transfer/nft-token-transfer'; const Info = () => { const { currentConfirmation } = useConfirmContext(); const ConfirmationInfoComponentMap = useMemo( () => ({ + [TransactionType.contractInteraction]: () => BaseTransactionInfo, + [TransactionType.deployContract]: () => BaseTransactionInfo, [TransactionType.personalSign]: () => PersonalSignInfo, + [TransactionType.simpleSend]: () => NativeTransferInfo, [TransactionType.signTypedData]: () => { const { version } = (currentConfirmation as SignatureRequestType)?.msgParams ?? {}; @@ -25,15 +29,13 @@ const Info = () => { } return TypedSignInfo; }, - [TransactionType.contractInteraction]: () => BaseTransactionInfo, - [TransactionType.deployContract]: () => BaseTransactionInfo, [TransactionType.tokenMethodApprove]: () => ApproveInfo, [TransactionType.tokenMethodIncreaseAllowance]: () => ApproveInfo, + [TransactionType.tokenMethodSafeTransferFrom]: () => NFTTokenTransferInfo, [TransactionType.tokenMethodSetApprovalForAll]: () => SetApprovalForAllInfo, [TransactionType.tokenMethodTransfer]: () => TokenTransferInfo, [TransactionType.tokenMethodTransferFrom]: () => NFTTokenTransferInfo, - [TransactionType.tokenMethodSafeTransferFrom]: () => NFTTokenTransferInfo, }), [currentConfirmation], ); diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/__snapshots__/native-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/native-transfer/__snapshots__/native-transfer.test.tsx.snap new file mode 100644 index 000000000000..234c0b704d5c --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/__snapshots__/native-transfer.test.tsx.snap @@ -0,0 +1,402 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NativeTransferInfo renders correctly 1`] = ` +
+
+
+ G +
+

+ 0 ETH +

+

+ 0 +

+
+
+
+
+
+ +

+ 0x2e0D7...5d09B +

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

+ Estimated changes +

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

+ You send +

+
+
+
+
+
+

+ - 4 +

+
+
+
+
+
+
+ E +
+

+ ETH +

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

+ Network +

+
+
+
+
+ G +
+

+ Goerli +

+
+
+
+
+
+

+ Interacting with +

+
+
+
+
+ +

+ 0x07614...3ad68 +

+
+
+
+
+
+
+
+
+

+ Network fee +

+
+
+ +
+
+
+
+
+

+ 0.0001 ETH +

+

+ $0.04 +

+ +
+
+
+
+
+

+ Speed +

+
+
+
+
+

+ 🦊 Market +

+

+ + ~ + 0 sec + +

+
+
+
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.stories.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.stories.tsx new file mode 100644 index 000000000000..1de48b2bb2a0 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { Box } from '../../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, +} from '../../../../../../helpers/constants/design-system'; +import configureStore from '../../../../../../store/store'; +import { ConfirmContextProvider } from '../../../../context/confirm'; +import NativeTransferInfo from './native-transfer'; + +const store = configureStore(getMockTokenTransferConfirmState({})); + +const Story = { + title: 'Components/App/Confirm/info/NativeTransferInfo', + component: NativeTransferInfo, + decorators: [ + (story: () => any) => ( + + + + {story()} + + + + ), + ], +}; + +export default Story; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.test.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.test.tsx new file mode 100644 index 000000000000..f4b2b4afab50 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.test.tsx @@ -0,0 +1,41 @@ +import { screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import { tEn } from '../../../../../../../test/lib/i18n-helpers'; +import NativeTransferInfo from './native-transfer'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +jest.mock('../../../../../../store/actions', () => ({ + ...jest.requireActual('../../../../../../store/actions'), + getGasFeeTimeEstimate: jest.fn().mockResolvedValue({ + lowerTimeBound: 0, + upperTimeBound: 60000, + }), +})); + +describe('NativeTransferInfo', () => { + it('renders correctly', async () => { + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore([])(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + await waitFor(() => { + expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); + }); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx new file mode 100644 index 000000000000..a2dd3ceaaa05 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx @@ -0,0 +1,37 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { useConfirmContext } from '../../../../context/confirm'; +import { SimulationDetails } from '../../../simulation-details'; +import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; +import NativeSendHeading from '../shared/native-send-heading/native-send-heading'; +import { TokenDetailsSection } from '../token-transfer/token-details-section'; +import { TransactionFlowSection } from '../token-transfer/transaction-flow-section'; + +const NativeTransferInfo = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const isWalletInitiated = transactionMeta.origin === 'metamask'; + + return ( + <> + + + {!isWalletInitiated && ( + + + + )} + + + + + ); +}; + +export default NativeTransferInfo; diff --git a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap index 8313befacc21..8bd4a73fe440 100644 --- a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap @@ -60,26 +60,35 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" >
-

- Estimated changes -

-
- +

+ Estimated changes +

+
+
+ +
+
diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx index 6fe06c81467a..e105493485ec 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx @@ -10,6 +10,8 @@ import { } from '../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import { signatureRequestSIWE } from '../../../../../../../test/data/confirmations/personal_sign'; +import * as utils from '../../../../utils'; +import * as snapUtils from '../../../../../../helpers/utils/snaps'; import PersonalSignInfo from './personal-sign'; jest.mock( @@ -21,6 +23,29 @@ jest.mock( }), ); +jest.mock('../../../../utils', () => { + const originalUtils = jest.requireActual('../../../../utils'); + return { + ...originalUtils, + isSIWESignatureRequest: jest.fn().mockReturnValue(false), + }; +}); + +jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { + const originalUtils = jest.requireActual( + '../../../../../../../node_modules/@metamask/snaps-utils', + ); + return { + ...originalUtils, + stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), + getSnapPrefix: jest.fn().mockReturnValue('npm:'), + }; +}); + +jest.mock('../../../../../../helpers/utils/snaps', () => ({ + isSnapId: jest.fn(), +})); + describe('PersonalSignInfo', () => { it('renders correctly for personal sign request', () => { const state = getMockPersonalSignConfirmState(); @@ -62,6 +87,7 @@ describe('PersonalSignInfo', () => { }); it('display signing in from for SIWE request', () => { + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(true); const state = getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); const mockStore = configureMockStore([])(state); @@ -73,6 +99,7 @@ describe('PersonalSignInfo', () => { }); it('display simulation for SIWE request if preference useTransactionSimulations is enabled', () => { + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(true); const state = getMockPersonalSignConfirmStateForRequest( signatureRequestSIWE, { @@ -88,4 +115,73 @@ describe('PersonalSignInfo', () => { ); expect(getByText('Estimated changes')).toBeDefined(); }); + + it('does not display tooltip text when isSIWE is true', async () => { + const state = + getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); // isSIWE is true + + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(true); + const mockStore = configureMockStore([])(state); + const { queryByText, getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = getByText('Request from'); + await requestFromLabel.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + + expect( + queryByText('This is the site asking for your signature.'), + ).toBeNull(); + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeNull(); + }); + + it('displays "requestFromInfoSnap" tooltip when isSIWE is false and origin is a snap', async () => { + const state = + getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); + + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + + const mockStore = configureMockStore([])(state); + const { queryByText, getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = getByText('Request from'); + await requestFromLabel.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeDefined(); + }); + + it('displays "requestFromInfo" tooltip when isSIWE is false and origin is not a snap', async () => { + const state = + getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + + const mockStore = configureMockStore([])(state); + const { getByText, queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = getByText('Request from'); + await requestFromLabel.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + + expect( + queryByText('This is the site asking for your signature.'), + ).toBeDefined(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx index 3199c3d108e0..5eb798439ca8 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx @@ -19,6 +19,7 @@ import { selectUseTransactionSimulations } from '../../../../selectors/preferenc import { isSIWESignatureRequest } from '../../../../utils'; import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { SIWESignInfo } from './siwe-sign'; const PersonalSignInfo: React.FC = () => { @@ -39,6 +40,15 @@ const PersonalSignInfo: React.FC = () => { hexToText(currentConfirmation.msgParams?.data), ); + let toolTipMessage; + if (!isSIWE) { + if (isSnapId(currentConfirmation.msgParams.origin)) { + toolTipMessage = t('requestFromInfoSnap'); + } else { + toolTipMessage = t('requestFromInfo'); + } + } + return ( <> {isSIWE && useTransactionSimulations && ( @@ -56,7 +66,7 @@ const PersonalSignInfo: React.FC = () => { alertKey={RowAlertKey.RequestFrom} ownerId={currentConfirmation.id} label={t('requestFrom')} - tooltip={isSIWE ? undefined : t('requestFromInfo')} + tooltip={toolTipMessage} > diff --git a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx new file mode 100644 index 000000000000..a3c2b91c9f8e --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx @@ -0,0 +1,118 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { BigNumber } from 'bignumber.js'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../../shared/constants/network'; +import { + AvatarToken, + AvatarTokenSize, + Box, + Text, +} from '../../../../../../../components/component-library'; +import Tooltip from '../../../../../../../components/ui/tooltip'; +import { getIntlLocale } from '../../../../../../../ducks/locale/locale'; +import { getConversionRate } from '../../../../../../../ducks/metamask/metamask'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../../../helpers/constants/design-system'; +import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay'; +import { useFiatFormatter } from '../../../../../../../hooks/useFiatFormatter'; +import { getMultichainNetwork } from '../../../../../../../selectors/multichain'; +import { useConfirmContext } from '../../../../../context/confirm'; +import { + formatAmount, + formatAmountMaxPrecision, +} from '../../../../simulation-details/formatAmount'; +import { toNonScientificString } from '../../hooks/use-token-values'; + +const NativeSendHeading = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const nativeAssetTransferValue = new BigNumber( + transactionMeta.txParams.value as string, + ).dividedBy(new BigNumber(10).pow(18)); + + const conversionRate = useSelector(getConversionRate); + const fiatValue = + conversionRate && + nativeAssetTransferValue && + new BigNumber(conversionRate) + .times(nativeAssetTransferValue, 10) + .toNumber(); + const fiatFormatter = useFiatFormatter(); + const fiatDisplayValue = + fiatValue && fiatFormatter(fiatValue, { shorten: true }); + + const multichainNetwork = useSelector(getMultichainNetwork); + const ticker = multichainNetwork?.network?.ticker; + + const locale = useSelector(getIntlLocale); + const roundedTransferValue = formatAmount(locale, nativeAssetTransferValue); + + const transferValue = toNonScientificString( + nativeAssetTransferValue.toNumber(), + ); + + const NetworkImage = ( + + ); + + const NativeAssetAmount = + roundedTransferValue === + `<${formatAmountMaxPrecision(locale, MIN_AMOUNT)}` ? ( + + + {`${roundedTransferValue} ${ticker}`} + + + ) : ( + + {`${roundedTransferValue} ${ticker}`} + + ); + + const NativeAssetFiatConversion = ( + + {fiatDisplayValue} + + ); + + return ( + + {NetworkImage} + {NativeAssetAmount} + {NativeAssetFiatConversion} + + ); +}; + +export default NativeSendHeading; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx index b6aff206a26a..3f6bd429d20b 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -19,8 +19,7 @@ import { TextVariant, } from '../../../../../../../helpers/constants/design-system'; import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay'; -import { getWatchedToken } from '../../../../../../../selectors'; -import { MultichainState } from '../../../../../../../selectors/multichain'; +import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; import { useConfirmContext } from '../../../../../context/confirm'; import { formatAmountMaxPrecision } from '../../../../simulation-details/formatAmount'; import { useTokenValues } from '../../hooks/use-token-values'; @@ -28,16 +27,11 @@ import { useTokenDetails } from '../../hooks/useTokenDetails'; import { ConfirmLoader } from '../confirm-loader/confirm-loader'; const SendHeading = () => { + const t = useI18nContext(); const { currentConfirmation: transactionMeta } = useConfirmContext(); const locale = useSelector(getIntlLocale); - const selectedToken = useSelector((state: MultichainState) => - getWatchedToken(transactionMeta)(state), - ); - const { tokenImage, tokenSymbol } = useTokenDetails( - transactionMeta, - selectedToken, - ); + const { tokenImage, tokenSymbol } = useTokenDetails(transactionMeta); const { decodedTransferValue, displayTransferValue, @@ -48,15 +42,17 @@ const SendHeading = () => { const TokenImage = ( ); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap index f7a672912907..05a1db6732f5 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap @@ -57,26 +57,35 @@ exports[`TokenTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between mm-box--align-items-center" >
-

- Estimated changes -

-
- +

+ Estimated changes +

+
+
+ +
+
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx index 6d686873ea1c..629d7d64df3a 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx @@ -1,4 +1,7 @@ -import { TransactionMeta } from '@metamask/transaction-controller'; +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; import React from 'react'; import { useSelector } from 'react-redux'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../shared/constants/network'; @@ -61,7 +64,7 @@ export const TokenDetailsSection = () => { ); - const tokenRow = ( + const tokenRow = transactionMeta.type !== TransactionType.simpleSend && ( { const addresses = value?.data[0].params.filter( (param) => param.type === 'address', ); - // sometimes there's more than one address, in which case we want the last one - const recipientAddress = addresses?.[addresses.length - 1].value; + const recipientAddress = + transactionMeta.type === TransactionType.simpleSend + ? transactionMeta.txParams.to + : // sometimes there's more than one address, in which case we want the last one + addresses?.[addresses.length - 1].value; if (pending) { return ; diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx index c6801dd91314..2b1e6969ddd5 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx @@ -5,6 +5,7 @@ import { TransactionType } from '@metamask/transaction-controller'; import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../test/data/confirmations/helper'; import { unapprovedTypedSignMsgV1 } from '../../../../../../../test/data/confirmations/typed_sign'; +import * as snapUtils from '../../../../../../helpers/utils/snaps'; import TypedSignInfoV1 from './typed-sign-v1'; jest.mock( @@ -16,6 +17,21 @@ jest.mock( }), ); +jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { + const originalUtils = jest.requireActual( + '../../../../../../../node_modules/@metamask/snaps-utils', + ); + return { + ...originalUtils, + stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), + getSnapPrefix: jest.fn().mockReturnValue('npm:'), + }; +}); + +jest.mock('../../../../../../helpers/utils/snaps', () => ({ + isSnapId: jest.fn(), +})); + describe('TypedSignInfo', () => { it('correctly renders typed sign data request', () => { const mockState = getMockTypedSignConfirmStateForRequest( @@ -42,4 +58,50 @@ describe('TypedSignInfo', () => { ); expect(container).toMatchInlineSnapshot(`
`); }); + + it('displays "requestFromInfoSnap" tooltip when origin is a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeDefined(); + }); + + it('displays "requestFromInfo" tooltip when origin is not a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the site asking for your signature.'), + ).toBeDefined(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx index b7bfdba16e8a..eb935d6bdef2 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx @@ -14,6 +14,7 @@ import { import { useConfirmContext } from '../../../../context/confirm'; import { ConfirmInfoRowTypedSignDataV1 } from '../../row/typed-sign-data-v1/typedSignDataV1'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; const TypedSignV1Info: React.FC = () => { const t = useI18nContext(); @@ -23,6 +24,9 @@ const TypedSignV1Info: React.FC = () => { return null; } + const toolTipMessage = isSnapId(currentConfirmation.msgParams?.origin) + ? t('requestFromInfoSnap') + : t('requestFromInfo'); const chainId = currentConfirmation.chainId as string; return ( @@ -32,7 +36,7 @@ const TypedSignV1Info: React.FC = () => { alertKey={RowAlertKey.RequestFrom} ownerId={currentConfirmation.id} label={t('requestFrom')} - tooltip={t('requestFromInfo')} + tooltip={toolTipMessage} > { }; }); +jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { + const originalUtils = jest.requireActual( + '../../../../../../../node_modules/@metamask/snaps-utils', + ); + return { + ...originalUtils, + stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), + getSnapPrefix: jest.fn().mockReturnValue('npm:'), + }; +}); + +jest.mock('../../../../../../helpers/utils/snaps', () => ({ + isSnapId: jest.fn(), +})); + describe('TypedSignInfo', () => { it('renders origin for typed sign data request', () => { const state = getMockTypedSignConfirmState(); @@ -127,4 +143,50 @@ describe('TypedSignInfo', () => { ); expect(container).toMatchSnapshot(); }); + + it('displays "requestFromInfoSnap" tooltip when origin is a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeDefined(); + }); + + it('displays "requestFromInfo" tooltip when origin is not a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the site asking for your signature.'), + ).toBeDefined(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx index c1830b05bd4a..fa5e61caef1f 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx @@ -22,6 +22,7 @@ import { fetchErc20Decimals } from '../../../../utils/token'; import { useConfirmContext } from '../../../../context/confirm'; import { selectUseTransactionSimulations } from '../../../../selectors/preferences'; import { ConfirmInfoRowTypedSignData } from '../../row/typed-sign-data/typedSignData'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { PermitSimulation } from './permit-simulation'; const TypedSignInfo: React.FC = () => { @@ -55,6 +56,9 @@ const TypedSignInfo: React.FC = () => { })(); }, [verifyingContract]); + const toolTipMessage = isSnapId(currentConfirmation.msgParams.origin) + ? t('requestFromInfoSnap') + : t('requestFromInfo'); const msgData = currentConfirmation.msgParams?.data as string; return ( @@ -73,7 +77,7 @@ const TypedSignInfo: React.FC = () => { alertKey={RowAlertKey.RequestFrom} ownerId={currentConfirmation.id} label={t('requestFrom')} - tooltip={t('requestFromInfo')} + tooltip={toolTipMessage} > diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx index fca355a9b063..f8d6b87e50db 100644 --- a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx +++ b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx @@ -1,18 +1,14 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { Hex } from '@metamask/utils'; import { Box } from '../../../../../components/component-library'; import { Toast } from '../../../../../components/multichain'; import { getLastInteractedConfirmationInfo, setLastInteractedConfirmationInfo, } from '../../../../../store/actions'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../../../selectors'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { selectNetworkConfigurationByChainId } from '../../../../../selectors'; const CHAIN_CHANGE_THRESHOLD_MILLISECONDS = 60 * 1000; // 1 Minute const TOAST_TIMEOUT_MILLISECONDS = 5 * 1000; // 5 Seconds @@ -22,12 +18,13 @@ const NetworkChangeToastLegacy = ({ }: { confirmation: { id: string; chainId: string }; }) => { - const chainId = useSelector(getCurrentChainId); - const newChainId = confirmation?.chainId ?? chainId; + const newChainId = confirmation?.chainId; const [toastVisible, setToastVisible] = useState(false); const t = useI18nContext(); - const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); - const network = networkConfigurations[newChainId as Hex]; + + const network = useSelector((state) => + selectNetworkConfigurationByChainId(state, newChainId), + ); const hideToast = useCallback(() => { setToastVisible(false); @@ -35,9 +32,11 @@ const NetworkChangeToastLegacy = ({ useEffect(() => { let isMounted = true; + if (!confirmation) { return undefined; } + (async () => { const lastInteractedConfirmationInfo = await getLastInteractedConfirmationInfo(); @@ -71,7 +70,7 @@ const NetworkChangeToastLegacy = ({ return () => { isMounted = false; }; - }, [confirmation?.id, chainId]); + }, [confirmation?.id]); if (!toastVisible) { return null; diff --git a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js index a0a16cc838c3..795401673691 100644 --- a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js +++ b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js @@ -39,7 +39,7 @@ export default function ContractDetailsModal({ tokenAddress, toAddress, chainId, - rpcPrefs, + blockExplorerUrl, tokenId, assetName, assetStandard, @@ -178,7 +178,7 @@ export default function ContractDetailsModal({ tokenAddress, chainId, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, null, ); @@ -287,7 +287,7 @@ export default function ContractDetailsModal({ toAddress, chainId, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, null, ); @@ -338,9 +338,9 @@ ContractDetailsModal.propTypes = { */ chainId: PropTypes.string, /** - * RPC prefs of the current network + * Block explorer URL of the current network */ - rpcPrefs: PropTypes.object, + blockExplorerUrl: PropTypes.string, /** * The token id of the NFT */ diff --git a/ui/pages/confirmations/components/multilayer-fee-message/__snapshots__/multi-layer-fee-message.test.js.snap b/ui/pages/confirmations/components/multilayer-fee-message/__snapshots__/multi-layer-fee-message.test.js.snap index 362926d71ce8..bf7516245e8b 100644 --- a/ui/pages/confirmations/components/multilayer-fee-message/__snapshots__/multi-layer-fee-message.test.js.snap +++ b/ui/pages/confirmations/components/multilayer-fee-message/__snapshots__/multi-layer-fee-message.test.js.snap @@ -105,6 +105,7 @@ exports[`Multi layer fee message when balance and token price checker is enabled > $0.00 @@ -152,6 +153,7 @@ exports[`Multi layer fee message when balance and token price checker is enabled > $0.56 diff --git a/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js b/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js index 4115f06cd644..ff5adfea8b5d 100644 --- a/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js +++ b/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { captureException } from '@sentry/browser'; import BlockaidPackage from '@blockaid/ppom_release/package.json'; -import { useSelector } from 'react-redux'; import { NETWORK_TO_NAME_MAP } from '../../../../../../shared/constants/network'; import { OverflowWrap } from '../../../../../helpers/constants/design-system'; import { I18nContext } from '../../../../../contexts/i18n'; @@ -20,7 +19,6 @@ import { useTransactionEventFragment } from '../../../hooks/useTransactionEventF import SecurityProviderBannerAlert from '../security-provider-banner-alert'; import LoadingIndicator from '../../../../../components/ui/loading-indicator'; -import { getCurrentChainId } from '../../../../../selectors'; import { getReportUrl } from './blockaid-banner-utils'; const zlib = require('zlib'); @@ -59,8 +57,6 @@ function BlockaidBannerAlert({ txData, ...props }) { const { securityAlertResponse, origin, msgParams, type, txParams, chainId } = txData; - const selectorChainId = useSelector(getCurrentChainId); - const t = useContext(I18nContext); const { updateTransactionEventFragment } = useTransactionEventFragment(); @@ -131,7 +127,7 @@ function BlockaidBannerAlert({ txData, ...props }) { const reportData = { blockNumber: block, blockaidVersion: BlockaidPackage.version, - chain: NETWORK_TO_NAME_MAP[chainId ?? selectorChainId], + chain: NETWORK_TO_NAME_MAP[chainId], classification: isFailedResultType ? 'error' : reason, domain: origin ?? msgParams?.origin ?? txParams?.origin, jsonRpcMethod: type, diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js index 9c91ca476ebb..61cbfe13e290 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js @@ -1,16 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; +import { RpcEndpointType } from '@metamask/network-controller'; +import { NetworkType } from '@metamask/controller-utils'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, - getCurrentChainId, getCurrentCurrency, + selectDefaultRpcEndpointByChainId, + selectNetworkConfigurationByChainId, } from '../../../../selectors'; import { formatCurrency } from '../../../../helpers/utils/confirm-tx.util'; import { @@ -26,22 +25,33 @@ import NetworkAccountBalanceHeader from '../../../../components/app/network-acco const SignatureRequestHeader = ({ txData }) => { const t = useI18nContext(); const { + chainId, msgParams: { from }, } = txData; const allAccounts = useSelector(accountsWithSendEtherInfoSelector); const fromAccount = getAccountByAddress(allAccounts, from); - const nativeCurrency = useSelector(getNativeCurrency); const currentCurrency = useSelector(getCurrentCurrency); - const currentChainId = useSelector(getCurrentChainId); - const providerConfig = useSelector(getProviderConfig); - const networkName = getNetworkNameFromProviderType(providerConfig.type); + const { nativeCurrency, name: networkNickname } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + + const defaultRpcEndpoint = useSelector((state) => + selectDefaultRpcEndpointByChainId(state, chainId), + ); + + const networkType = + defaultRpcEndpoint.type === RpcEndpointType.Custom + ? NetworkType.rpc + : defaultRpcEndpoint.networkClientId; + + const networkName = getNetworkNameFromProviderType(networkType); const conversionRate = null; // setting conversion rate to null by default to display balance in native const currentNetwork = networkName === '' - ? providerConfig.nickname || t('unknownNetwork') + ? networkNickname || t('unknownNetwork') : t(networkName); const balanceInBaseAsset = conversionRate @@ -71,7 +81,7 @@ const SignatureRequestHeader = ({ txData }) => { conversionRate ? currentCurrency?.toUpperCase() : nativeCurrency } accountAddress={fromAccount.address} - chainId={currentChainId} + chainId={chainId} /> ); }; diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js index 673b9a67e598..111be8932207 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js @@ -1,13 +1,30 @@ import React from 'react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Provider } from 'react-redux'; +import configureStore from '../../../../store/store'; +import testData from '../../../../../.storybook/test-data'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestHeader from './signature-request-header'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + +const store = configureStore({ + ...testData, + metamask: { + ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}); + export default { title: 'Confirmations/Components/SignatureRequestHeader', + decorators: [(story) => {story()}], argTypes: { txData: { control: 'object' }, }, args: { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: JSON.stringify({ diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js index a3ddd3136627..862339239643 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js @@ -1,12 +1,17 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestHeader from '.'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + const props = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', }, @@ -14,7 +19,10 @@ const props = { }; describe('SignatureRequestHeader', () => { - const store = configureMockStore()(mockState); + const store = configureMockStore()({ + ...mockState, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }); it('should match snapshot', () => { const { container } = renderWithProvider( diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js index 297978bb5f04..7343d5c34140 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js @@ -1,10 +1,16 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Provider } from 'react-redux'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import testData from '../../../../../.storybook/test-data'; +import configureStore from '../../../../store/store'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import README from './README.mdx'; import SignatureRequestOriginal from './signature-request-original.component'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const [MOCK_PRIMARY_ACCOUNT, MOCK_SECONDARY_ACCOUNT] = Object.values( testData.metamask.internalAccounts.accounts, ); @@ -41,9 +47,17 @@ const MOCK_SIGN_DATA = JSON.stringify({ }, }); +const store = configureStore({ + ...testData, + metamask: { + ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}); + export default { title: 'Confirmations/Components/SignatureRequestOriginal', - + decorators: [(story) => {story()}], component: SignatureRequestOriginal, parameters: { docs: { @@ -87,6 +101,7 @@ DefaultStory.storyName = 'personal_sign Type'; DefaultStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: MOCK_SIGN_DATA, @@ -102,6 +117,7 @@ ETHSignTypedStory.storyName = 'eth_signTypedData Type'; ETHSignTypedStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: [ @@ -128,6 +144,7 @@ AccountMismatchStory.storyName = 'Account Mismatch warning'; AccountMismatchStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', data: MOCK_SIGN_DATA, diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js index 11577f44e312..b8d1429751e6 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js @@ -3,6 +3,7 @@ import configureMockStore from 'redux-mock-store'; import { fireEvent, screen } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { EthAccountType } from '@metamask/keyring-api'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider'; import mockState from '../../../../../test/data/mock-state.json'; @@ -11,6 +12,7 @@ import configureStore from '../../../../store/store'; import { rejectPendingApproval } from '../../../../store/actions'; import { shortenAddress } from '../../../../helpers/utils/util'; import { ETH_EOA_METHODS } from '../../../../../shared/constants/eth-methods'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestOriginal from '.'; jest.mock('../../../../store/actions', () => ({ @@ -21,12 +23,15 @@ jest.mock('../../../../store/actions', () => ({ setLastInteractedConfirmationInfo: jest.fn(), })); +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + const address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; const props = { signMessage: jest.fn(), cancelMessage: jest.fn(), txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: address, data: [ @@ -76,6 +81,7 @@ const render = ({ txData = props.txData, selectedAccount } = {}) => { const store = configureStore({ metamask: { ...mockState.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), internalAccounts, }, }); @@ -120,6 +126,7 @@ describe('SignatureRequestOriginal', () => { it('should escape RTL character in label or value', () => { const txData = { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', data: [ diff --git a/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap b/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap index a524cbcc1caf..fcf0bb30d8a3 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap @@ -169,7 +169,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`] > 966.987986 - ETH + GoerliETH
diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js index de29680421c6..7900cfe1828d 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js @@ -1,18 +1,37 @@ import React from 'react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Provider } from 'react-redux'; import testData from '../../../../../.storybook/test-data'; +import configureStore from '../../../../store/store'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import README from './README.mdx'; import SignatureRequestSIWE from './signature-request-siwe'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + +const TRANSACTION_DATA_MOCK = { + chainId: CHAIN_ID_MOCK, +}; + const { internalAccounts: { accounts, selectedAccount }, } = testData.metamask; + const otherAccount = Object.values(accounts)[1]; const { address: selectedAddress } = accounts[selectedAccount]; +const store = configureStore({ + ...testData, + metamask: { + ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}); + export default { title: 'Confirmations/Components/SignatureRequestSIWE', - + decorators: [(story) => {story()}], component: SignatureRequestSIWE, parameters: { docs: { @@ -134,6 +153,7 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams, }, }; @@ -144,6 +164,7 @@ export const BadDomainStory = (args) => { BadDomainStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams: badDomainParams, }, }; @@ -154,6 +175,7 @@ export const BadAddressStory = (args) => { BadAddressStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams: badAddressParams, }, }; @@ -164,6 +186,7 @@ export const BadDomainAndAddressStory = (args) => { BadDomainAndAddressStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams: badDomainAndAddressParams, }, }; diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js index c97be33e45b0..f8f1a67b8cbc 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js @@ -2,14 +2,16 @@ import React from 'react'; import { cloneDeep } from 'lodash'; import { fireEvent } from '@testing-library/react'; import { ApprovalType } from '@metamask/controller-utils'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import configureStore from '../../../../store/store'; -import { getCurrentChainId } from '../../../../selectors'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestSIWE from '.'; const MOCK_ORIGIN = 'https://example-dapp.website'; const MOCK_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; const mockStoreInitialState = { metamask: { @@ -20,6 +22,7 @@ const mockStoreInitialState = { name: 'Example Test Dapp', }, }, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), }, }; @@ -37,6 +40,7 @@ const mockProps = { cancelPersonalMessage: jest.fn(), signPersonalMessage: jest.fn(), txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: MOCK_ADDRESS, data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', @@ -183,7 +187,7 @@ describe('SignatureRequestSIWE (Sign in with Ethereum)', () => { transactions: [ ...mockStoreInitialState.metamask.transactions, { - chainId: getCurrentChainId(mockStoreInitialState), + chainId: CHAIN_ID_MOCK, status: 'unapproved', }, ], diff --git a/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap b/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap index ba272fc26d89..943f8699b9aa 100644 --- a/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap +++ b/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap @@ -177,26 +177,6 @@ exports[`Signature Request Component render should match snapshot when we are us
-
- -
-

- To view and confirm your most recent request, you'll need to approve or reject existing requests first. -

-

-

-
@@ -962,7 +942,6 @@ exports[`Signature Request Component render should match snapshot when we want t > 0 - ETH
@@ -970,26 +949,6 @@ exports[`Signature Request Component render should match snapshot when we want t
-
- -
-

- To view and confirm your most recent request, you'll need to approve or reject existing requests first. -

-

-

-
diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js index 3cf2a54327c9..2a7977da0c75 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.js @@ -18,16 +18,14 @@ import { doesAddressRequireLedgerHidConnection, getSubjectMetadata, getTotalUnapprovedMessagesCount, + selectNetworkConfigurationByChainId, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) accountsWithSendEtherInfoSelector, getSelectedAccount, getAccountType, ///: END:ONLY_INCLUDE_IF } from '../../../../selectors'; -import { - getProviderConfig, - isAddressLedger, -} from '../../../../ducks/metamask/metamask'; +import { isAddressLedger } from '../../../../ducks/metamask/metamask'; import { sanitizeMessage, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -97,6 +95,7 @@ const SignatureRequest = ({ txData, warnings }) => { const { id, type, + chainId, msgParams: { from, data, origin, version }, } = txData; @@ -104,7 +103,12 @@ const SignatureRequest = ({ txData, warnings }) => { const hardwareWalletRequiresConnection = useSelector((state) => doesAddressRequireLedgerHidConnection(state, from), ); - const { chainId, rpcPrefs } = useSelector(getProviderConfig); + + const { blockExplorerUrls } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + + const blockExplorerUrl = blockExplorerUrls?.[0]; const unapprovedMessagesCount = useSelector(getTotalUnapprovedMessagesCount); const subjectMetadata = useSelector(getSubjectMetadata); const isLedgerWallet = useSelector((state) => isAddressLedger(state, from)); @@ -337,7 +341,7 @@ const SignatureRequest = ({ txData, warnings }) => { setShowContractDetails(false)} isContractRequestingSignature /> diff --git a/ui/pages/confirmations/components/signature-request/signature-request.stories.js b/ui/pages/confirmations/components/signature-request/signature-request.stories.js index b31032f8b1a7..cc5374e2becb 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.stories.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.stories.js @@ -1,21 +1,25 @@ import React from 'react'; import { Provider } from 'react-redux'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import configureStore from '../../../../store/store'; import testData from '../../../../../.storybook/test-data'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import README from './README.mdx'; import SignatureRequest from './signature-request'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const store = configureStore({ ...testData, metamask: { ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), }, }); export default { title: 'Confirmations/Components/SignatureRequest', decorators: [(story) => {story()}], - component: SignatureRequest, parameters: { docs: { @@ -35,6 +39,7 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: JSON.stringify({ @@ -82,6 +87,7 @@ AccountMismatchStory.storyName = 'AccountMismatch'; AccountMismatchStory.args = { ...DefaultStory.args, txData: { + chainId: CHAIN_ID_MOCK, msgParams: { ...DefaultStory.args.txData.msgParams, from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', diff --git a/ui/pages/confirmations/components/signature-request/signature-request.test.js b/ui/pages/confirmations/components/signature-request/signature-request.test.js index 9851cdbef454..2e10381c40cb 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.test.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.test.js @@ -1,35 +1,25 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import { EthAccountType } from '@metamask/keyring-api'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../../ducks/metamask/metamask'; -import { - accountsWithSendEtherInfoSelector, - conversionRateSelector, - getCurrentCurrency, - getMemoizedAddressBook, - getPreferences, - getSelectedAccount, - getTotalUnapprovedMessagesCount, - getInternalAccounts, - unconfirmedTransactionsHashSelector, - getAccountType, - getMemoizedMetaMaskInternalAccounts, - getSelectedInternalAccount, - pendingApprovalsSortedSelector, - getNetworkConfigurationsByChainId, -} from '../../../../selectors'; import { ETH_EOA_METHODS } from '../../../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequest from './signature-request'; +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => jest.fn(), +})); + +const CHAIN_ID_MOCK = '0x539'; + +const TRANSACTION_DATA_MOCK = { + chainId: CHAIN_ID_MOCK, +}; + const baseProps = { clearConfirmTransaction: () => jest.fn(), cancel: () => jest.fn(), @@ -37,8 +27,11 @@ const baseProps = { showRejectTransactionsConfirmationModal: () => jest.fn(), sign: () => jest.fn(), }; + const mockStore = { + ...mockState, metamask: { + ...mockState.metamask, ...mockNetworkState({ chainId: '0x539', nickname: 'Localhost 8545', @@ -61,12 +54,13 @@ const mockStore = { metadata: { name: 'John Doe', keyring: { - type: 'HD Key Tree', + type: 'Custody', }, }, options: {}, methods: ETH_EOA_METHODS, type: EthAccountType.Eoa, + balance: 0, }, }, selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', @@ -94,15 +88,6 @@ const mockStore = { }, }, }; -jest.mock('react-redux', () => { - const actual = jest.requireActual('react-redux'); - - return { - ...actual, - useSelector: jest.fn(), - useDispatch: () => jest.fn(), - }; -}); const mockCustodySignFn = jest.fn(); @@ -114,62 +99,13 @@ jest.mock('../../../../hooks/useMMICustodySignMessage', () => ({ jest.mock('@metamask-institutional/extension'); -const generateUseSelectorRouter = (opts) => (selector) => { - const mockSelectedInternalAccount = getSelectedInternalAccount(opts); - - switch (selector) { - case getProviderConfig: - return getProviderConfig(opts); - case getCurrentCurrency: - return opts.metamask.currentCurrency; - case getNativeCurrency: - return getProviderConfig(opts).ticker; - case getTotalUnapprovedMessagesCount: - return opts.metamask.unapprovedTypedMessagesCount; - case getPreferences: - return opts.metamask.preferences; - case conversionRateSelector: - return opts.metamask.currencyRates[getProviderConfig(opts).ticker] - ?.conversionRate; - case getSelectedAccount: - return mockSelectedInternalAccount; - case getInternalAccounts: - return Object.values(opts.metamask.internalAccounts.accounts); - case getMemoizedMetaMaskInternalAccounts: - return Object.values(opts.metamask.internalAccounts.accounts); - case getMemoizedAddressBook: - return []; - case accountsWithSendEtherInfoSelector: - return Object.values(opts.metamask.internalAccounts.accounts).map( - (internalAccount) => { - return { - ...internalAccount, - ...(opts.metamask.accounts[internalAccount.address] ?? {}), - balance: - opts.metamask.accounts[internalAccount.address]?.balance ?? 0, - }; - }, - ); - case getAccountType: - return 'custody'; - case unconfirmedTransactionsHashSelector: - return {}; - case pendingApprovalsSortedSelector: - return Object.values(opts.metamask.pendingApprovals); - case getNetworkConfigurationsByChainId: - return opts.metamask.networkConfigurationsByChainId; - default: - return undefined; - } -}; describe('Signature Request Component', () => { - const store = configureMockStore()(mockState); + const store = configureMockStore()(mockStore); describe('render', () => { let messageData; beforeEach(() => { - useSelector.mockImplementation(generateUseSelectorRouter(mockStore)); messageData = { domain: { chainId: 97, @@ -219,35 +155,39 @@ describe('Signature Request Component', () => { }); it('should match snapshot when we want to switch to fiat', () => { - useSelector.mockImplementation( - generateUseSelectorRouter({ - ...mockStore, - metamask: { - ...mockStore.metamask, - currencyRates: { - ...mockStore.metamask.currencyRates, - ETH: { - ...(mockStore.metamask.currencyRates.ETH || {}), - conversionRate: 231.06, - }, + const storeOverride = configureMockStore()({ + ...mockStore, + metamask: { + ...mockStore.metamask, + ...mockNetworkState({ + chainId: CHAIN_ID_MOCK, + }), + currencyRates: { + ...mockStore.metamask.currencyRates, + ETH: { + ...(mockStore.metamask.currencyRates.ETH || {}), + conversionRate: 231.06, }, }, - }), - ); + }, + }); + const msgParams = { from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', data: JSON.stringify(messageData), version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , - store, + storeOverride, ); expect(container).toMatchSnapshot(); @@ -260,10 +200,12 @@ describe('Signature Request Component', () => { version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , @@ -280,10 +222,12 @@ describe('Signature Request Component', () => { version: 'V4', origin: 'test', }; + const { queryByTestId } = renderWithProvider( , @@ -308,6 +252,7 @@ describe('Signature Request Component', () => { , @@ -321,29 +266,30 @@ describe('Signature Request Component', () => { }); it('should not render a reject multiple requests link if there is not multiple requests', () => { - useSelector.mockImplementation( - generateUseSelectorRouter({ - ...mockStore, - metamask: { - ...mockStore.metamask, - unapprovedTypedMessagesCount: 0, - }, - }), - ); + const storeOverride = configureMockStore()({ + ...mockStore, + metamask: { + ...mockStore.metamask, + unapprovedTypedMessagesCount: 0, + }, + }); + const msgParams = { from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', data: JSON.stringify(messageData), version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , - store, + storeOverride, ); expect( @@ -358,10 +304,12 @@ describe('Signature Request Component', () => { version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , @@ -384,6 +332,7 @@ describe('Signature Request Component', () => { , @@ -408,6 +357,7 @@ describe('Signature Request Component', () => { , @@ -429,6 +379,7 @@ describe('Signature Request Component', () => { { { }); it('should render a warning when the selected account is not the one being used to sign', () => { - const msgParams = { - from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', - data: JSON.stringify(messageData), - version: 'V4', - origin: 'test', - }; - - useSelector.mockImplementation( - generateUseSelectorRouter({ - ...mockStore, - metamask: { - ...mockStore.metamask, + const storeOverride = configureMockStore()({ + ...mockStore, + metamask: { + ...mockStore.metamask, + accounts: { + ...mockStore.metamask.accounts, + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + balance: '0x0', + name: 'Account 1', + }, + '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + balance: '0x0', + name: 'Account 2', + }, + }, + internalAccounts: { accounts: { - ...mockStore.metamask.accounts, - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1': { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - balance: '0x0', - name: 'Account 1', - }, - '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { - address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', - balance: '0x0', - name: 'Account 2', - }, - }, - internalAccounts: { - accounts: { - 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', - metadata: { - name: 'Account 1', - keyring: { - type: 'HD Key Tree', - }, + id: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, }, - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 2', - keyring: { - type: 'HD Key Tree', - }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, + }, + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, }, - selectedAccount: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', }, + selectedAccount: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', }, - }), - ); + }, + }); + + const msgParams = { + from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + data: JSON.stringify(messageData), + version: 'V4', + origin: 'test', + }; const { container } = renderWithProvider( , - store, + storeOverride, ); expect( @@ -571,6 +522,7 @@ describe('Signature Request Component', () => { {...baseProps} conversionRate={null} txData={{ + ...TRANSACTION_DATA_MOCK, msgParams, securityAlertResponse: { resultType: 'Malicious', @@ -602,6 +554,7 @@ describe('Signature Request Component', () => { , @@ -610,7 +563,9 @@ describe('Signature Request Component', () => { const rejectRequestsLink = getByTestId('page-container-footer-next'); fireEvent.click(rejectRequestsLink); - expect(mockCustodySignFn).toHaveBeenCalledWith({ msgParams }); + expect(mockCustodySignFn).toHaveBeenCalledWith( + expect.objectContaining({ msgParams }), + ); }); }); }); diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx index 5bb182ac7e66..339ffbdd6a1a 100644 --- a/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx +++ b/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx @@ -26,6 +26,21 @@ jest.mock('./balance-change-list', () => ({ jest.mock('./useSimulationMetrics'); +jest.mock( + '../../../../components/app/confirm/info/row/alert-row/alert-row', + () => ({ + ConfirmInfoAlertRow: jest.fn(({ label }) => <>{label}), + }), +); + +jest.mock('../../context/confirm', () => ({ + useConfirmContext: jest.fn(() => ({ + currentConfirmation: { + id: 'testTransactionId', + }, + })), +})); + const renderSimulationDetails = (simulationData?: Partial) => renderWithProvider( { ); }; +const HeaderWithAlert = ({ transactionId }: { transactionId: string }) => { + const t = useI18nContext(); + + return ( + + {/* Intentional fragment */} + <> + + ); +}; + +const LegacyHeader = () => { + const t = useI18nContext(); + return ( + + + {t('simulationDetailsTitle')} + + + + + + ); +}; + /** * Header at the top of the simulation preview. * * @param props * @param props.children + * @param props.isTransactionsRedesign + * @param props.transactionId */ -const HeaderLayout: React.FC = ({ children }) => { - const t = useI18nContext(); +const HeaderLayout: React.FC<{ + isTransactionsRedesign: boolean; + transactionId: string; +}> = ({ children, isTransactionsRedesign, transactionId }) => { return ( { alignItems={AlignItems.center} justifyContent={JustifyContent.spaceBetween} > - - - {t('simulationDetailsTitle')} - - - - - + {isTransactionsRedesign ? ( + + ) : ( + + )} {children} ); @@ -142,11 +179,13 @@ const HeaderLayout: React.FC = ({ children }) => { * @param props.inHeader * @param props.isTransactionsRedesign * @param props.children + * @param props.transactionId */ const SimulationDetailsLayout: React.FC<{ inHeader?: React.ReactNode; isTransactionsRedesign: boolean; -}> = ({ inHeader, isTransactionsRedesign, children }) => ( + transactionId: string; +}> = ({ inHeader, isTransactionsRedesign, transactionId, children }) => ( - {inHeader} + + {inHeader} + {children} ); @@ -199,6 +243,7 @@ export const SimulationDetails: React.FC = ({ } isTransactionsRedesign={isTransactionsRedesign} + transactionId={transactionId} > ); } @@ -216,7 +261,10 @@ export const SimulationDetails: React.FC = ({ if (error) { return ( - + ); @@ -226,7 +274,10 @@ export const SimulationDetails: React.FC = ({ const empty = balanceChanges.length === 0; if (empty) { return ( - + ); @@ -235,7 +286,10 @@ export const SimulationDetails: React.FC = ({ const outgoing = balanceChanges.filter((bc) => bc.amount.isNegative()); const incoming = balanceChanges.filter((bc) => !bc.amount.isNegative()); return ( - + 0 @@ -469,6 +470,7 @@ exports[`ConfirmSendEther should render correct information for for confirm send > 0.000021 @@ -522,6 +524,7 @@ exports[`ConfirmSendEther should render correct information for for confirm send > 0.00021 diff --git a/ui/pages/confirmations/confirm-signature-request/index.test.js b/ui/pages/confirmations/confirm-signature-request/index.test.js index e0c7c7c8bbeb..f634bf40845c 100644 --- a/ui/pages/confirmations/confirm-signature-request/index.test.js +++ b/ui/pages/confirmations/confirm-signature-request/index.test.js @@ -7,13 +7,21 @@ import { CHAIN_IDS } from '../../../../shared/constants/network'; import { mockNetworkState } from '../../../../test/stub/networks'; import ConfTx from '.'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + const mockState = { metamask: { + ...mockNetworkState({ + chainId: CHAIN_IDS.GOERLI, + nickname: 'Goerli test network', + ticker: undefined, + }), unapprovedPersonalMsgs: {}, unapprovedPersonalMsgCount: 0, unapprovedTypedMessages: { 267460284130106: { id: 267460284130106, + chainId: CHAIN_ID_MOCK, msgParams: { data: '{"domain":{"chainId":"5","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"}],"Group":[{"name":"name","type":"string"},{"name":"members","type":"Person[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}', from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', @@ -27,11 +35,6 @@ const mockState = { }, }, unapprovedTypedMessagesCount: 1, - ...mockNetworkState({ - chainId: CHAIN_IDS.GOERLI, - nickname: 'Goerli test network', - ticker: undefined, - }), currencyRates: {}, keyrings: [], subjectMetadata: {}, diff --git a/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap b/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap index c89fa090cd3d..3bd2313850a4 100644 --- a/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap +++ b/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap @@ -264,6 +264,7 @@ exports[`Confirm Transaction Base should match snapshot 1`] = ` > 0.0001 @@ -407,6 +408,7 @@ exports[`Confirm Transaction Base should match snapshot 1`] = ` > 0.000021 @@ -440,6 +442,7 @@ exports[`Confirm Transaction Base should match snapshot 1`] = ` > 0.000021 diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js index 156bca192523..12971f21a2af 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js @@ -126,9 +126,10 @@ const ConfirmTransaction = () => { const prevTransactionId = usePrevious(transactionId); usePolling({ - startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + startPolling: (input) => + gasFeeStartPollingByNetworkClientId(input.networkClientId), stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - networkClientId: transaction.networkClientId ?? networkClientId, + input: { networkClientId: transaction.networkClientId ?? networkClientId }, }); useEffect(() => { diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap index a541bb5f7ae3..317071e6be26 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap @@ -334,6 +334,7 @@ exports[`remove-snap-account confirmation should match snapshot 1`] = ` > 966.988 @@ -377,6 +378,7 @@ exports[`remove-snap-account confirmation should match snapshot 1`] = ` > 966.988 diff --git a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js index 24d66b8785fb..1df546b06003 100644 --- a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js +++ b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js @@ -11,7 +11,10 @@ import { Severity, TypographyVariant, } from '../../../../helpers/constants/design-system'; -import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes'; +import { + DEFAULT_ROUTE, + ONBOARDING_PRIVACY_SETTINGS_ROUTE, +} from '../../../../helpers/constants/routes'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils'; import { isValidASCIIURL, toPunycodeURL } from '../../utils/confirm'; @@ -381,7 +384,13 @@ function getValues(pendingApproval, t, actions, history, data) { nickname: pendingApproval.requestData.chainName, }); - history.push(DEFAULT_ROUTE); + const locationPath = document.location.hash.replace('#', '/'); + const isOnboardingRoute = + locationPath === ONBOARDING_PRIVACY_SETTINGS_ROUTE; + + if (!isOnboardingRoute) { + history.push(DEFAULT_ROUTE); + } } return []; }, diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useResimulationAlert.test.ts b/ui/pages/confirmations/hooks/alerts/transactions/useResimulationAlert.test.ts new file mode 100644 index 000000000000..174b6ec0398a --- /dev/null +++ b/ui/pages/confirmations/hooks/alerts/transactions/useResimulationAlert.test.ts @@ -0,0 +1,126 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { + TransactionMeta, + TransactionStatus, + TransactionType, +} from '@metamask/transaction-controller'; + +import { getMockConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { renderHookWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; +import { Severity } from '../../../../../helpers/constants/design-system'; +import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants'; +import { genUnapprovedContractInteractionConfirmation } from '../../../../../../test/data/confirmations/contract-interaction'; +import { useResimulationAlert } from './useResimulationAlert'; + +const ACCOUNT_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; +const TRANSACTION_ID_MOCK = '123-456'; + +const CONFIRMATION_MOCK = genUnapprovedContractInteractionConfirmation({ + chainId: '0x5', +}) as TransactionMeta; + +const TRANSACTION_META_MOCK = { + id: TRANSACTION_ID_MOCK, + chainId: '0x5', + status: TransactionStatus.submitted, + type: TransactionType.contractInteraction, + txParams: { + from: ACCOUNT_ADDRESS, + }, + time: new Date().getTime() - 10000, + simulationData: { + isUpdatedAfterSecurityCheck: true, + tokenBalanceChanges: [], + }, +} as TransactionMeta; + +function runHook({ + currentConfirmation, + transactions = [], +}: { + currentConfirmation?: TransactionMeta; + transactions?: TransactionMeta[]; +} = {}) { + let pendingApprovals = {}; + if (currentConfirmation) { + pendingApprovals = { + [currentConfirmation.id as string]: { + id: currentConfirmation.id, + type: ApprovalType.Transaction, + }, + }; + transactions.push(currentConfirmation); + } + const state = getMockConfirmState({ + metamask: { + pendingApprovals, + transactions, + }, + }); + const response = renderHookWithConfirmContextProvider( + useResimulationAlert, + state, + ); + + return response.result.current; +} + +describe('useResimulationAlert', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns no alerts if no confirmation', () => { + expect(runHook()).toEqual([]); + }); + + it('returns no alerts if no transactions', () => { + expect( + runHook({ + currentConfirmation: CONFIRMATION_MOCK, + transactions: [], + }), + ).toEqual([]); + }); + + it('returns no alerts if isUpdatedAfterSecurityCheck is false', () => { + const notResimulatedConfirmation = { + ...TRANSACTION_META_MOCK, + simulationData: { + isUpdatedAfterSecurityCheck: false, + tokenBalanceChanges: [], + }, + }; + expect( + runHook({ + currentConfirmation: notResimulatedConfirmation, + }), + ).toEqual([]); + }); + + it('returns alert if isUpdatedAfterSecurityCheck is true', () => { + const resimulatedConfirmation = { + ...CONFIRMATION_MOCK, + simulationData: { + isUpdatedAfterSecurityCheck: true, + tokenBalanceChanges: [], + }, + }; + const alerts = runHook({ + currentConfirmation: resimulatedConfirmation, + }); + + expect(alerts).toEqual([ + { + actions: [], + field: RowAlertKey.Resimulation, + isBlocking: false, + key: 'simulationDetailsTitle', + message: + 'Estimated changes for this transaction have been updated. Review them closely before proceeding.', + reason: 'Results have changed', + severity: Severity.Danger, + }, + ]); + }); +}); diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useResimulationAlert.ts b/ui/pages/confirmations/hooks/alerts/transactions/useResimulationAlert.ts new file mode 100644 index 000000000000..c838e07e62c4 --- /dev/null +++ b/ui/pages/confirmations/hooks/alerts/transactions/useResimulationAlert.ts @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { TransactionMeta } from '@metamask/transaction-controller'; + +import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { Severity } from '../../../../../helpers/constants/design-system'; +import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants'; +import { useConfirmContext } from '../../../context/confirm'; + +export function useResimulationAlert(): Alert[] { + const t = useI18nContext(); + const { currentConfirmation } = useConfirmContext(); + + const isUpdatedAfterSecurityCheck = (currentConfirmation as TransactionMeta) + ?.simulationData?.isUpdatedAfterSecurityCheck; + + return useMemo(() => { + if (!isUpdatedAfterSecurityCheck) { + return []; + } + + return [ + { + actions: [], + field: RowAlertKey.Resimulation, + isBlocking: false, + key: 'simulationDetailsTitle', + message: t('alertMessageChangeInSimulationResults'), + reason: t('alertReasonChangeInSimulationResults'), + severity: Severity.Danger, + }, + ]; + }, [isUpdatedAfterSecurityCheck, t]); +} diff --git a/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts b/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts index 1b6412d66614..2f26fcefbee9 100644 --- a/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts @@ -15,7 +15,6 @@ import { import { Alert } from '../../../../ducks/confirm-alerts/confirm-alerts'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { getCurrentChainId } from '../../../../selectors'; import { SIGNATURE_TRANSACTION_TYPES, REDESIGN_DEV_TRANSACTION_TYPES, @@ -51,7 +50,6 @@ type SecurityAlertResponsesState = { const useBlockaidAlerts = (): Alert[] => { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); - const selectorChainId = useSelector(getCurrentChainId); const securityAlertId = ( currentConfirmation?.securityAlertResponse as SecurityAlertResponse @@ -99,9 +97,7 @@ const useBlockaidAlerts = (): Alert[] => { const reportData = { blockNumber: block, blockaidVersion: BlockaidPackage.version, - chain: (NETWORK_TO_NAME_MAP as Record)[ - chainId ?? selectorChainId - ], + chain: (NETWORK_TO_NAME_MAP as Record)[chainId], classification: isFailedResultType ? 'error' : reason, domain: origin ?? msgParams?.origin ?? origin, jsonRpcMethod: type, diff --git a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts index 3ea9a5e2d254..c5f77f143cb6 100644 --- a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts +++ b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts @@ -10,6 +10,7 @@ import { useNetworkBusyAlerts } from './alerts/transactions/useNetworkBusyAlerts import { useNoGasPriceAlerts } from './alerts/transactions/useNoGasPriceAlerts'; import { usePendingTransactionAlerts } from './alerts/transactions/usePendingTransactionAlerts'; import { useQueuedConfirmationsAlerts } from './alerts/transactions/useQueuedConfirmationsAlerts'; +import { useResimulationAlert } from './alerts/transactions/useResimulationAlert'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { useSigningOrSubmittingAlerts } from './alerts/transactions/useSigningOrSubmittingAlerts'; ///: END:ONLY_INCLUDE_IF @@ -34,11 +35,11 @@ function useTransactionAlerts(): Alert[] { const networkBusyAlerts = useNetworkBusyAlerts(); const noGasPriceAlerts = useNoGasPriceAlerts(); const pendingTransactionAlerts = usePendingTransactionAlerts(); + const resimulationAlert = useResimulationAlert(); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const signingOrSubmittingAlerts = useSigningOrSubmittingAlerts(); ///: END:ONLY_INCLUDE_IF const queuedConfirmationsAlerts = useQueuedConfirmationsAlerts(); - return useMemo( () => [ ...gasEstimateFailedAlerts, @@ -48,6 +49,7 @@ function useTransactionAlerts(): Alert[] { ...networkBusyAlerts, ...noGasPriceAlerts, ...pendingTransactionAlerts, + ...resimulationAlert, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) ...signingOrSubmittingAlerts, ///: END:ONLY_INCLUDE_IF @@ -61,6 +63,7 @@ function useTransactionAlerts(): Alert[] { networkBusyAlerts, noGasPriceAlerts, pendingTransactionAlerts, + resimulationAlert, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) signingOrSubmittingAlerts, ///: END:ONLY_INCLUDE_IF diff --git a/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts b/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts index 4c6dddfa7a5a..f0463287ef34 100644 --- a/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts +++ b/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts @@ -6,30 +6,23 @@ import { NETWORK_TO_NAME_MAP, } from '../../../../shared/constants/network'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../selectors'; - import { useI18nContext } from '../../../hooks/useI18nContext'; import { useConfirmContext } from '../context/confirm'; +import { selectNetworkConfigurationByChainId } from '../../../selectors'; function useConfirmationNetworkInfo() { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); - const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); - const currentChainId = useSelector(getCurrentChainId); + const chainId = currentConfirmation?.chainId as Hex; + + const networkConfiguration = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); let networkDisplayName = ''; let networkImageUrl = ''; if (currentConfirmation) { - // use the current confirmation chainId, else use the current network chainId - const chainId = - (currentConfirmation?.chainId as Hex | undefined) ?? currentChainId; - - const networkConfiguration = networkConfigurations[chainId]; - networkDisplayName = networkConfiguration?.name ?? NETWORK_TO_NAME_MAP[chainId as keyof typeof NETWORK_TO_NAME_MAP] ?? diff --git a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts index 8ce37261aa8e..cf5e8a1383a2 100644 --- a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts +++ b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts @@ -8,7 +8,6 @@ import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { ApprovalsMetaMaskState, - getCurrentChainId, getIsRedesignedConfirmationsDeveloperEnabled, getRedesignedConfirmationsEnabled, getRedesignedTransactionsEnabled, @@ -36,9 +35,6 @@ const useCurrentConfirmation = () => { const oldestPendingApproval = useSelector(oldestPendingConfirmationSelector); const confirmationId = paramsConfirmationId ?? oldestPendingApproval?.id; - // TODO: Temporary pending chain ID persisted in signature requests. - const globalChainId = useSelector(getCurrentChainId); - const isRedesignedSignaturesUserSettingEnabled = useSelector( getRedesignedConfirmationsEnabled, ); @@ -106,12 +102,10 @@ const useCurrentConfirmation = () => { } const currentConfirmation = - transactionMetadata ?? - (signatureMessage && { ...signatureMessage, chainId: globalChainId }) ?? - undefined; + transactionMetadata ?? signatureMessage ?? undefined; return { currentConfirmation }; - }, [transactionMetadata, signatureMessage, shouldUseRedesign, globalChainId]); + }, [transactionMetadata, signatureMessage, shouldUseRedesign]); }; export default useCurrentConfirmation; diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts index dff0103fbe21..9c98432918e2 100644 --- a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useContext } from 'react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { TokenStandard } from '../../../../shared/constants/transaction'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { TokenDetailsERC20 } from '../utils/token'; @@ -30,7 +31,7 @@ describe('useTrackERC20WithoutDecimalInformation', () => { }); renderHook(() => - useTrackERC20WithoutDecimalInformation('0x5', { + useTrackERC20WithoutDecimalInformation(CHAIN_IDS.MAINNET, '0x5', { standard: TokenStandard.ERC20, } as TokenDetailsERC20), ); diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts index fa6a5e620fc4..266048e11134 100644 --- a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts @@ -1,4 +1,3 @@ -import { useSelector } from 'react-redux'; import { useContext, useEffect } from 'react'; import { Hex } from '@metamask/utils'; @@ -10,23 +9,23 @@ import { } from '../../../../shared/constants/metametrics'; import { TokenStandard } from '../../../../shared/constants/transaction'; import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { getCurrentChainId } from '../../../selectors'; import { parseTokenDetailDecimals, TokenDetailsERC20 } from '../utils/token'; /** * Track event that number of decimals in ERC20 is not obtained * + * @param chainId * @param tokenAddress * @param tokenDetails * @param metricLocation */ const useTrackERC20WithoutDecimalInformation = ( + chainId: Hex, tokenAddress: Hex | string | undefined, tokenDetails?: TokenDetailsERC20, metricLocation = MetaMetricsEventLocation.SignatureConfirmation, ) => { const trackEvent = useContext(MetaMetricsContext); - const chainId = useSelector(getCurrentChainId); useEffect(() => { if (chainId === undefined || tokenDetails === undefined) { diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index e33ff79d6f01..a007ca0aa0b2 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -25,6 +25,7 @@ export const REDESIGN_USER_TRANSACTION_TYPES = [ TransactionType.tokenMethodTransfer, TransactionType.tokenMethodTransferFrom, TransactionType.tokenMethodSafeTransferFrom, + TransactionType.simpleSend, ]; export const REDESIGN_DEV_TRANSACTION_TYPES = [ diff --git a/ui/pages/index.js b/ui/pages/index.js index 0b1cdcef78cd..c30846fff1e6 100644 --- a/ui/pages/index.js +++ b/ui/pages/index.js @@ -10,7 +10,7 @@ import { LegacyMetaMetricsProvider, } from '../contexts/metametrics'; import { MetamaskNotificationsProvider } from '../contexts/metamask-notifications'; -import { CurrencyRateProvider } from '../contexts/currencyRate'; +import { AssetPollingProvider } from '../contexts/assetPolling'; import ErrorPage from './error'; import Routes from './routes'; @@ -49,11 +49,11 @@ class Index extends PureComponent { - + - + diff --git a/ui/pages/institutional/account-list/account-list.tsx b/ui/pages/institutional/account-list/account-list.tsx index e710a0e6e93a..84431f867307 100644 --- a/ui/pages/institutional/account-list/account-list.tsx +++ b/ui/pages/institutional/account-list/account-list.tsx @@ -1,6 +1,6 @@ import React from 'react'; import CustodyLabels from '../../../components/institutional/custody-labels'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { shortenAddress } from '../../../helpers/utils/util'; import Tooltip from '../../../components/ui/tooltip'; @@ -41,7 +41,7 @@ type CustodyAccountListProps = { }; const getButtonLinkHref = (account: Account) => { - const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; + const url = CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; return `${url}address/${account.address}`; }; diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx index b2db09b4d06c..6fca1af16a8e 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom'; import thunk from 'redux-thunk'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { shortenAddress } from '../../../helpers/utils/util'; import { getSelectedInternalAccountFromMockState } from '../../../../test/jest/mocks'; @@ -145,7 +145,7 @@ describe('Interactive Replacement Token Page', function () { it('should render all the accounts correctly', async () => { const expectedHref = `${ - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET] + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET] }address/${custodianAddress}`; await act(async () => { diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx index c7258d1b2e38..01ec70c93025 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx @@ -27,9 +27,9 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { getMetaMaskAccounts } from '../../../selectors'; import { getInstitutionalConnectRequests } from '../../../ducks/institutional/institutional'; -import { getSelectedInternalAccount } from '../../../selectors/selectors'; +import { getSelectedInternalAccount } from '../../../selectors/accounts'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { mmiActionsFactory, @@ -43,7 +43,7 @@ import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { shortenAddress } from '../../../helpers/utils/util'; const getButtonLinkHref = ({ address }: { address: string }) => { - const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; + const url = CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; return `${url}address/${address}`; }; @@ -154,6 +154,7 @@ const InteractiveReplacementTokenPage: React.FC = () => { const filteredAccounts = custodianAccounts.filter( (account: TokenAccount) => + // @ts-expect-error metaMaskAccounts isn't a real type metaMaskAccounts[account.address.toLowerCase()], ); @@ -163,6 +164,7 @@ const InteractiveReplacementTokenPage: React.FC = () => { name: account.name, labels: account.labels, balance: + // @ts-expect-error metaMaskAccounts isn't a real type metaMaskAccounts[account.address.toLowerCase()]?.balance || 0, }), ); diff --git a/ui/pages/notifications-settings/notifications-settings.tsx b/ui/pages/notifications-settings/notifications-settings.tsx index d929f048a793..0fafb468b733 100644 --- a/ui/pages/notifications-settings/notifications-settings.tsx +++ b/ui/pages/notifications-settings/notifications-settings.tsx @@ -57,7 +57,7 @@ export default function NotificationsSettings() { const isUpdatingMetamaskNotifications = useSelector( getIsUpdatingMetamaskNotifications, ); - const accounts: AccountType[] = useSelector(getInternalAccounts); + const accounts = useSelector(getInternalAccounts) as AccountType[]; // States const [loadingAllowNotifications, setLoadingAllowNotifications] = diff --git a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap index ad53f67a7127..bd156c765969 100644 --- a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap +++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap @@ -396,7 +396,7 @@ exports[`ConnectPage should render with defaults from the requested permissions - Requesting for + Requesting for Custom Mainnet RPC
diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 50aea4e0dc60..132b97f7caa9 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -40,9 +40,10 @@ export default class AdvancedTab extends PureComponent { setUseNonceField: PropTypes.func, useNonceField: PropTypes.bool, setHexDataFeatureFlag: PropTypes.func, - displayWarning: PropTypes.func, + displayErrorInSettings: PropTypes.func, + hideErrorInSettings: PropTypes.func, showResetAccountConfirmationModal: PropTypes.func, - warning: PropTypes.string, + errorInSettings: PropTypes.string, sendHexData: PropTypes.bool, showFiatInTestnets: PropTypes.bool, showTestNetworks: PropTypes.bool, @@ -80,7 +81,9 @@ export default class AdvancedTab extends PureComponent { componentDidMount() { const { t } = this.context; + const { hideErrorInSettings } = this.props; handleSettingsRefs(t, t('advanced'), this.settingsRefs); + hideErrorInSettings(); } async getTextFromFile(file) { @@ -112,7 +115,7 @@ export default class AdvancedTab extends PureComponent { renderStateLogs() { const { t } = this.context; - const { displayWarning } = this.props; + const { displayErrorInSettings } = this.props; return ( { - window.logStateString((err, result) => { + window.logStateString(async (err, result) => { if (err) { - displayWarning(t('stateLogError')); + displayErrorInSettings(t('stateLogError')); } else { - exportAsFile( - `${t('stateLogFileName')}.json`, - result, - ExportableContentType.JSON, - ); + try { + await exportAsFile( + `${t('stateLogFileName')}.json`, + result, + ExportableContentType.JSON, + ); + } catch (error) { + displayErrorInSettings(error.message); + } } }); }} @@ -576,11 +584,13 @@ export default class AdvancedTab extends PureComponent { } render() { - const { warning } = this.props; + const { errorInSettings } = this.props; // When adding/removing/editing the order of renders, double-check the order of the settingsRefs. This affects settings-search.js return (
- {warning ?
{warning}
: null} + {errorInSettings ? ( +
{errorInSettings}
+ ) : null} {this.renderStateLogs()} {this.renderResetAccount()} {this.renderToggleStxOptIn()} diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js index 2c64b79e4f4d..aa5dc11ace89 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js @@ -1,15 +1,17 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import mockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { exportAsFile } from '../../../helpers/utils/export-utils'; import AdvancedTab from '.'; const mockSetAutoLockTimeLimit = jest.fn().mockReturnValue({ type: 'TYPE' }); const mockSetShowTestNetworks = jest.fn(); const mockSetShowFiatConversionOnTestnetsPreference = jest.fn(); const mockSetStxPrefEnabled = jest.fn(); +const mockDisplayErrorInSettings = jest.fn(); jest.mock('../../../store/actions.ts', () => { return { @@ -21,6 +23,32 @@ jest.mock('../../../store/actions.ts', () => { }; }); +jest.mock('../../../ducks/app/app.ts', () => ({ + displayErrorInSettings: () => mockDisplayErrorInSettings, + hideErrorInSettings: () => jest.fn(), +})); + +jest.mock('../../../helpers/utils/export-utils', () => ({ + ...jest.requireActual('../../../helpers/utils/export-utils'), + exportAsFile: jest + .fn() + .mockResolvedValueOnce({}) + .mockImplementationOnce(new Error('state file error')), +})); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + getPlatformInfo: jest.fn().mockResolvedValue('mac'), + }, +})); + +Object.defineProperty(window, 'stateHooks', { + value: { + getCleanAppState: () => mockState, + getLogs: () => [], + }, +}); + describe('AdvancedTab Component', () => { const mockStore = configureMockStore([thunk])(mockState); @@ -105,4 +133,33 @@ describe('AdvancedTab Component', () => { expect(mockSetStxPrefEnabled).toHaveBeenCalled(); }); }); + + describe('renderStateLogs', () => { + it('should render the toggle button for state log download', () => { + const { queryByTestId } = renderWithProvider(, mockStore); + const stateLogButton = queryByTestId('advanced-setting-state-logs'); + expect(stateLogButton).toBeInTheDocument(); + }); + + it('should call exportAsFile when the toggle button is clicked', async () => { + const { queryByTestId } = renderWithProvider(, mockStore); + const stateLogButton = queryByTestId( + 'advanced-setting-state-logs-button', + ); + fireEvent.click(stateLogButton); + await waitFor(() => { + expect(exportAsFile).toHaveBeenCalledTimes(1); + }); + }); + it('should call displayErrorInSettings when the state file download fails', async () => { + const { queryByTestId } = renderWithProvider(, mockStore); + const stateLogButton = queryByTestId( + 'advanced-setting-state-logs-button', + ); + fireEvent.click(stateLogButton); + await waitFor(() => { + expect(mockDisplayErrorInSettings).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index f2ad894d1e8b..aaa094e0655c 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -5,7 +5,6 @@ import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../../shared/constants/prefe import { getPreferences } from '../../../selectors'; import { backupUserData, - displayWarning, setAutoLockTimeLimit, setDismissSeedBackUpReminder, setFeatureFlag, @@ -17,11 +16,15 @@ import { showModal, } from '../../../store/actions'; import { getSmartTransactionsPreferenceEnabled } from '../../../../shared/modules/selectors'; +import { + displayErrorInSettings, + hideErrorInSettings, +} from '../../../ducks/app/app'; import AdvancedTab from './advanced-tab.component'; export const mapStateToProps = (state) => { const { - appState: { warning }, + appState: { errorInSettings }, metamask, } = state; const { @@ -37,7 +40,7 @@ export const mapStateToProps = (state) => { } = getPreferences(state); return { - warning, + errorInSettings, sendHexData, showFiatInTestnets, showTestNetworks, @@ -54,7 +57,9 @@ export const mapDispatchToProps = (dispatch) => { backupUserData: () => backupUserData(), setHexDataFeatureFlag: (shouldShow) => dispatch(setFeatureFlag('sendHexData', shouldShow)), - displayWarning: (warning) => dispatch(displayWarning(warning)), + displayErrorInSettings: (errorInSettings) => + dispatch(displayErrorInSettings(errorInSettings)), + hideErrorInSettings: () => dispatch(hideErrorInSettings()), showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), setUseNonceField: (value) => dispatch(setUseNonceField(value)), diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js index 46e7ee978f43..36c84cbba8c0 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.stories.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js @@ -22,7 +22,8 @@ export default { setDismissSeedBackUpReminder: { action: 'setDismissSeedBackUpReminder' }, setUseNonceField: { action: 'setUseNonceField' }, setHexDataFeatureFlag: { action: 'setHexDataFeatureFlag' }, - displayWarning: { action: 'displayWarning' }, + displayErrorInSettings: { action: 'displayErrorInSettings' }, + hideErrorInSettings: { action: 'hideErrorInSettings' }, history: { action: 'history' }, showResetAccountConfirmationModal: { action: 'showResetAccountConfirmationModal', diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index dcec71767fe6..0927d04f89cb 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -83,11 +83,6 @@ exports[`Security Tab should match snapshot 1`] = `
-
- warning -
diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index 1fae729d3f31..f9e854ff2465 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -64,7 +64,6 @@ export default class SecurityTab extends PureComponent { }; static propTypes = { - warning: PropTypes.string, history: PropTypes.object, openSeaEnabled: PropTypes.bool, setOpenSeaEnabled: PropTypes.func, @@ -1131,7 +1130,6 @@ export default class SecurityTab extends PureComponent { render() { const { - warning, petnamesEnabled, dataCollectionForMarketing, setDataCollectionForMarketing, @@ -1144,8 +1142,6 @@ export default class SecurityTab extends PureComponent { {showDataCollectionDisclaimer ? this.renderDataCollectionWarning() : null} - - {warning &&
{warning}
} {this.context.t('security')} diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index 224072ef2b10..676a53097d4a 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -27,15 +27,12 @@ import { getNetworkConfigurationsByChainId, getMetaMetricsDataDeletionId, getPetnamesEnabled, -} from '../../../selectors'; +} from '../../../selectors/selectors'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; import SecurityTab from './security-tab.component'; const mapStateToProps = (state) => { - const { - appState: { warning }, - metamask, - } = state; + const { metamask } = state; const petnamesEnabled = getPetnamesEnabled(state); @@ -60,7 +57,6 @@ const mapStateToProps = (state) => { const networkConfigurations = getNetworkConfigurationsByChainId(state); return { - warning, incomingTransactionsPreferences, networkConfigurations, participateInMetaMetrics, diff --git a/ui/pages/settings/security-tab/security-tab.test.js b/ui/pages/settings/security-tab/security-tab.test.js index 1685c5417151..ac93efc2e324 100644 --- a/ui/pages/settings/security-tab/security-tab.test.js +++ b/ui/pages/settings/security-tab/security-tab.test.js @@ -48,8 +48,6 @@ jest.mock('../../../ducks/app/app.ts', () => { }); describe('Security Tab', () => { - mockState.appState.warning = 'warning'; // This tests an otherwise untested render branch - const mockStore = configureMockStore([thunk])(mockState); function renderWithProviders(ui, store) { diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js index 191bbbc78685..6d56cd9ae10b 100644 --- a/ui/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/pages/settings/settings-tab/settings-tab.component.js @@ -57,7 +57,6 @@ export default class SettingsTab extends PureComponent { static propTypes = { setUseBlockie: PropTypes.func, setCurrentCurrency: PropTypes.func, - warning: PropTypes.string, updateCurrentLocale: PropTypes.func, currentLocale: PropTypes.string, useBlockie: PropTypes.bool, @@ -429,11 +428,8 @@ export default class SettingsTab extends PureComponent { } render() { - const { warning } = this.props; - return (
- {warning ?
{warning}
: null} {this.renderCurrentConversion()} {this.renderShowNativeTokenAsMainBalance()} {this.renderCurrentLocale()} diff --git a/ui/pages/settings/settings-tab/settings-tab.container.js b/ui/pages/settings/settings-tab/settings-tab.container.js index 7de17ffefde4..e6ad25f0df92 100644 --- a/ui/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/pages/settings/settings-tab/settings-tab.container.js @@ -18,10 +18,7 @@ import { getProviderConfig } from '../../../ducks/metamask/metamask'; import SettingsTab from './settings-tab.component'; const mapStateToProps = (state) => { - const { - appState: { warning }, - metamask, - } = state; + const { metamask } = state; const { currentCurrency, useBlockie, currentLocale } = metamask; const { ticker: nativeCurrency } = getProviderConfig(state); const { address: selectedAddress } = getSelectedInternalAccount(state); @@ -31,7 +28,6 @@ const mapStateToProps = (state) => { const tokenList = getTokenList(state); return { - warning, currentLocale, currentCurrency, nativeCurrency, diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index 7c410ca03ce5..111af726acfa 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -48,8 +48,8 @@ import { QUOTES_NOT_AVAILABLE_ERROR, CONTRACT_DATA_DISABLED_ERROR, OFFLINE_FOR_MAINTENANCE, - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; import PulseLoader from '../../../components/ui/pulse-loader'; @@ -143,7 +143,7 @@ export default function AwaitingSwap({ }; const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; const blockExplorerUrl = getBlockExplorerLink( { hash: txHash, chainId }, diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js index 8a701289bebd..1e5eb5179e2c 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -2,9 +2,9 @@ import React, { useContext, useEffect, useState, useCallback } from 'react'; import BigNumber from 'bignumber.js'; import PropTypes from 'prop-types'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import { uniqBy, isEqual } from 'lodash'; +import { uniqBy, isEqual, isEmpty } from 'lodash'; import { useHistory } from 'react-router-dom'; -import { getTokenTrackerLink } from '@metamask/etherscan-link'; +import { getAccountLink, getTokenTrackerLink } from '@metamask/etherscan-link'; import classnames from 'classnames'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -83,23 +83,23 @@ import { usePrevious } from '../../../hooks/usePrevious'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from '../../../../shared/modules/swaps.utils'; +import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils'; import { MetaMetricsEventCategory, MetaMetricsEventLinkType, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, TokenBucketPriority, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, QUOTES_EXPIRED_ERROR, MAX_ALLOWED_SLIPPAGE, } from '../../../../shared/constants/swaps'; +import { + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, + CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP, +} from '../../../../shared/constants/common'; import { resetSwapsPostFetchState, ignoreTokens, @@ -142,6 +142,7 @@ import SwapsBannerAlert from '../swaps-banner-alert/swaps-banner-alert'; import SwapsFooter from '../swaps-footer'; import SelectedToken from '../selected-token/selected-token'; import ListWithSearch from '../list-with-search/list-with-search'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; import QuotesLoadingAnimation from './quotes-loading-animation'; import ReviewQuote from './review-quote'; @@ -228,8 +229,8 @@ export default function PrepareSwapPage({ const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); - const fetchParamsFromToken = isSwapsDefaultTokenSymbol( - sourceTokenInfo?.symbol, + const fetchParamsFromToken = isSwapsDefaultTokenAddress( + sourceTokenInfo?.address, chainId, ) ? defaultSwapsToken @@ -241,7 +242,8 @@ export default function PrepareSwapPage({ // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that // the balance of the token can appear in the from token selection dropdown const fromTokenArray = - !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance + !isSwapsDefaultTokenAddress(fromToken?.address, chainId) && + fromToken?.balance ? [fromToken] : []; const usersTokens = uniqBy( @@ -310,7 +312,10 @@ export default function PrepareSwapPage({ { showFiat: true }, true, ); - const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) + const swapFromFiatValue = isSwapsDefaultTokenAddress( + fromTokenAddress, + chainId, + ) ? swapFromEthFiatValue : swapFromTokenFiatValue; @@ -435,19 +440,27 @@ export default function PrepareSwapPage({ onInputChange(fromTokenInputValue, token.string, token.decimals); }; - const blockExplorerTokenLink = getTokenTrackerLink( - selectedToToken.address, - chainId, - null, // no networkId - null, // no holderAddress - { - blockExplorerUrl: - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, - }, - ); + const blockExplorerTokenLink = + chainId === CHAIN_IDS.ZKSYNC_ERA + ? // Use getAccountLink because zksync explorer uses a /address URL scheme instead of /token + getAccountLink(selectedToToken.address, chainId, { + blockExplorerUrl: + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, + }) + : getTokenTrackerLink( + selectedToToken.address, + chainId, + null, // no networkId + null, // no holderAddress + { + blockExplorerUrl: + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, + }, + ); const blockExplorerLabel = rpcPrefs.blockExplorerUrl - ? getURLHostName(blockExplorerTokenLink) + ? CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP[chainId] ?? + t('etherscan') : t('etherscan'); const { address: toAddress } = toToken || {}; @@ -786,18 +799,23 @@ export default function PrepareSwapPage({ ); } - const isNonDefaultToken = !isSwapsDefaultTokenSymbol( - fromTokenSymbol, + const isNonDefaultFromToken = !isSwapsDefaultTokenAddress( + fromTokenAddress, chainId, ); const hasPositiveFromTokenBalance = rawFromTokenBalance > 0; const isTokenEligibleForMaxBalance = - isSmartTransaction || (!isSmartTransaction && isNonDefaultToken); + isSmartTransaction || (!isSmartTransaction && isNonDefaultFromToken); const showMaxBalanceLink = fromTokenSymbol && isTokenEligibleForMaxBalance && hasPositiveFromTokenBalance; + const isNonDefaultToToken = !isSwapsDefaultTokenAddress( + selectedToToken.address, + chainId, + ); + return (
@@ -1024,6 +1042,21 @@ export default function PrepareSwapPage({ {selectedToToken?.string && yourTokenToBalance}
+ +
+ {selectedToToken && + !isEmpty(selectedToToken) && + isNonDefaultToToken && + t('swapTokenVerifiedSources', [ + occurrences, + , + ])} +
+
{showCrossChainSwapsLink && ( { }, }); const props = createProps(); - const { getByText } = renderWithProvider( + const { getByText, getAllByText } = renderWithProvider( , store, ); @@ -128,7 +128,7 @@ describe('PrepareSwapPage', () => { expect( getByText('USDC is only verified on 1 source', { exact: false }), ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); + expect(getAllByText('Etherscan')[0]).toBeInTheDocument(); expect(getByText('Continue swapping')).toBeInTheDocument(); }); @@ -143,7 +143,7 @@ describe('PrepareSwapPage', () => { }, }); const props = createProps(); - const { getByText } = renderWithProvider( + const { getByText, getAllByText } = renderWithProvider( , store, ); @@ -151,7 +151,7 @@ describe('PrepareSwapPage', () => { expect( getByText('Verify this token on', { exact: false }), ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); + expect(getAllByText('Etherscan')[0]).toBeInTheDocument(); expect(getByText('Continue swapping')).toBeInTheDocument(); }); @@ -167,11 +167,11 @@ describe('PrepareSwapPage', () => { }, }); const props = createProps(); - const { getByText } = renderWithProvider( + const { getAllByText } = renderWithProvider( , store, ); - const blockExplorer = getByText('etherscan.io'); + const blockExplorer = getAllByText('Etherscan')[0]; expect(blockExplorer).toBeInTheDocument(); fireEvent.click(blockExplorer); expect(global.platform.openTab).toHaveBeenCalledWith({ diff --git a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js index 779d1edf58a8..bd1bb5aa5aaf 100644 --- a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js +++ b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js @@ -13,7 +13,7 @@ import { getUseCurrencyRateCheck, } from '../../../../selectors'; import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/common'; import { getURLHostName } from '../../../../helpers/utils/util'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; @@ -35,7 +35,7 @@ export default function ItemList({ const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const blockExplorerLink = rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const blockExplorerHostName = getURLHostName(blockExplorerLink); diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index 7b8d5910c218..d3127c9a94f3 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -23,7 +23,7 @@ import { getSmartTransactionsEnabled, getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { DEFAULT_ROUTE, PREPARE_SWAP_ROUTE, @@ -87,7 +87,7 @@ export default function SmartTransactionStatusPage() { ); const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; let smartTransactionStatus = SmartTransactionStatus.pending; diff --git a/ui/selectors/accounts.test.ts b/ui/selectors/accounts.test.ts index 61a0059989ba..033d88c30faa 100644 --- a/ui/selectors/accounts.test.ts +++ b/ui/selectors/accounts.test.ts @@ -1,3 +1,5 @@ +import { EthAccountType } from '@metamask/keyring-api'; +import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { MOCK_ACCOUNTS, MOCK_ACCOUNT_EOA, @@ -5,12 +7,15 @@ import { MOCK_ACCOUNT_BIP122_P2WPKH, MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, } from '../../test/data/mock-accounts'; +import mockState from '../../test/data/mock-state.json'; import { AccountsState, isSelectedInternalAccountEth, isSelectedInternalAccountBtc, hasCreatedBtcMainnetAccount, hasCreatedBtcTestnetAccount, + getSelectedInternalAccount, + getInternalAccounts, } from './accounts'; const MOCK_STATE: AccountsState = { @@ -23,6 +28,83 @@ const MOCK_STATE: AccountsState = { }; describe('Accounts Selectors', () => { + describe('#getInternalAccounts', () => { + it('returns a list of internal accounts', () => { + expect(getInternalAccounts(mockState as AccountsState)).toStrictEqual( + Object.values(mockState.metamask.internalAccounts.accounts), + ); + }); + }); + + describe('#getSelectedInternalAccount', () => { + it('returns selected internalAccount', () => { + expect( + getSelectedInternalAccount(mockState as AccountsState), + ).toStrictEqual({ + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + importTime: 0, + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + }); + }); + + it('returns undefined if selectedAccount is undefined', () => { + expect( + getSelectedInternalAccount({ + metamask: { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }, + }), + ).toBeUndefined(); + }); + + it('returns selectedAccount', () => { + const mockInternalAccount = { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + importTime: 0, + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, + }; + expect( + getSelectedInternalAccount({ + metamask: { + internalAccounts: { + accounts: { + [mockInternalAccount.id]: mockInternalAccount, + }, + selectedAccount: mockInternalAccount.id, + }, + }, + }), + ).toStrictEqual(mockInternalAccount); + }); + }); + describe('isSelectedInternalAccountEth', () => { // @ts-expect-error This is missing from the Mocha type definitions it.each([ diff --git a/ui/selectors/accounts.ts b/ui/selectors/accounts.ts index bd33d5af1f89..af977b7511da 100644 --- a/ui/selectors/accounts.ts +++ b/ui/selectors/accounts.ts @@ -8,7 +8,6 @@ import { isBtcMainnetAddress, isBtcTestnetAddress, } from '../../shared/lib/multichain'; -import { getSelectedInternalAccount, getInternalAccounts } from './selectors'; export type AccountsState = { metamask: AccountsControllerState; @@ -20,6 +19,15 @@ function isBtcAccount(account: InternalAccount) { return Boolean(account && account.type === P2wpkh); } +export function getInternalAccounts(state: AccountsState) { + return Object.values(state.metamask.internalAccounts.accounts); +} + +export function getSelectedInternalAccount(state: AccountsState) { + const accountId = state.metamask.internalAccounts.selectedAccount; + return state.metamask.internalAccounts.accounts[accountId]; +} + export function isSelectedInternalAccountEth(state: AccountsState) { const account = getSelectedInternalAccount(state); const { Eoa, Erc4337 } = EthAccountType; diff --git a/ui/selectors/index.js b/ui/selectors/index.js index 6c65a481a709..290c70fb2a31 100644 --- a/ui/selectors/index.js +++ b/ui/selectors/index.js @@ -7,3 +7,4 @@ export * from './permissions'; export * from './selectors'; export * from './transactions'; export * from './approvals'; +export * from './accounts'; diff --git a/ui/selectors/institutional/selectors.ts b/ui/selectors/institutional/selectors.ts index edaaf5278ae7..05bd13b52509 100644 --- a/ui/selectors/institutional/selectors.ts +++ b/ui/selectors/institutional/selectors.ts @@ -1,5 +1,6 @@ import { toChecksumAddress } from 'ethereumjs-util'; -import { getAccountType, getSelectedInternalAccount } from '../selectors'; +import { getAccountType } from '../selectors'; +import { getSelectedInternalAccount } from '../accounts'; import { getProviderConfig } from '../../ducks/metamask/metamask'; import { hexToDecimal } from '../../../shared/modules/conversion.utils'; // TODO: Remove restricted import @@ -166,6 +167,7 @@ export function getCustodianIconForAddress(state: State, address: string) { export function getIsCustodianSupportedChain(state: State) { try { + // @ts-expect-error state types don't match const selectedAccount = getSelectedInternalAccount(state); const accountType = getAccountType(state); const providerConfig = getProviderConfig(state); @@ -207,6 +209,7 @@ export function getIsCustodianSupportedChain(state: State) { export function getMMIAddressFromModalOrAddress(state: State) { const modalAddress = state?.appState?.modal?.modalState?.props?.address; + // @ts-expect-error state types don't match const selectedAddress = getSelectedInternalAccount(state)?.address; return modalAddress || selectedAddress; diff --git a/ui/selectors/multichain.test.ts b/ui/selectors/multichain.test.ts index 19fdac1559a7..3097d61f9549 100644 --- a/ui/selectors/multichain.test.ts +++ b/ui/selectors/multichain.test.ts @@ -105,7 +105,7 @@ function getEvmState(chainId: Hex = CHAIN_IDS.MAINNET): TestState { rates: { btc: { conversionDate: 0, - conversionRate: '100000', + conversionRate: 100000, }, }, }, diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 308dc104b6d6..96266687b6df 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -24,7 +24,7 @@ import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, TEST_NETWORK_IDS, } from '../../shared/constants/network'; -import { AccountsState } from './accounts'; +import { AccountsState, getSelectedInternalAccount } from './accounts'; import { getCurrentChainId, getCurrentCurrency, @@ -33,7 +33,6 @@ import { getNativeCurrencyImage, getNetworkConfigurationsByChainId, getSelectedAccountCachedBalance, - getSelectedInternalAccount, getShouldShowFiat, getShowFiatInTestnets, } from './selectors'; diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index 4031b8e881a3..00468f2f948d 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -10,9 +10,9 @@ import { getInternalAccount, getMetaMaskAccountsOrdered, getOriginOfCurrentTab, - getSelectedInternalAccount, getTargetSubjectMetadata, } from './selectors'; +import { getSelectedInternalAccount } from './accounts'; // selectors diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 8522b561b5b1..8b44dd715aba 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -117,6 +117,7 @@ import { getOrderedConnectedAccountsForConnectedDapp, getSubjectMetadata, } from './permissions'; +import { getSelectedInternalAccount, getInternalAccounts } from './accounts'; import { createDeepEqualSelector } from './util'; import { getMultichainBalances, getMultichainNetwork } from './multichain'; @@ -353,11 +354,6 @@ export function getMaybeSelectedInternalAccount(state) { : undefined; } -export function getSelectedInternalAccount(state) { - const accountId = state.metamask.internalAccounts.selectedAccount; - return state.metamask.internalAccounts.accounts[accountId]; -} - export function checkIfMethodIsEnabled(state, methodName) { const internalAccount = getSelectedInternalAccount(state); return Boolean(internalAccount.methods.includes(methodName)); @@ -375,10 +371,6 @@ export function getSelectedInternalAccountWithBalance(state) { return selectedAccountWithBalance; } -export function getInternalAccounts(state) { - return Object.values(state.metamask.internalAccounts.accounts); -} - export function getInternalAccount(state, accountId) { return state.metamask.internalAccounts.accounts[accountId]; } @@ -586,11 +578,27 @@ export const getTokenExchangeRates = (state) => { ); }; +/** + * Get market data for tokens on the current chain + * + * @param state + * @returns {Record} + */ export const getTokensMarketData = (state) => { const chainId = getCurrentChainId(state); return state.metamask.marketData?.[chainId]; }; +/** + * Get market data for tokens across all chains + * + * @param state + * @returns {Record>} + */ +export const getMarketData = (state) => { + return state.metamask.marketData; +}; + export function getAddressBook(state) { const chainId = getCurrentChainId(state); if (!state.metamask.addressBook[chainId]) { @@ -704,6 +712,28 @@ export const getNetworkConfigurationsByChainId = createDeepEqualSelector( (networkConfigurationsByChainId) => networkConfigurationsByChainId, ); +/** + * @type (state: any, chainId: string) => import('@metamask/network-controller').NetworkConfiguration + */ +export const selectNetworkConfigurationByChainId = createSelector( + getNetworkConfigurationsByChainId, + (_state, chainId) => chainId, + (networkConfigurationsByChainId, chainId) => + networkConfigurationsByChainId[chainId], +); + +export const selectDefaultRpcEndpointByChainId = createSelector( + selectNetworkConfigurationByChainId, + (networkConfiguration) => { + if (!networkConfiguration) { + return undefined; + } + + const { defaultRpcEndpointIndex, rpcEndpoints } = networkConfiguration; + return rpcEndpoints[defaultRpcEndpointIndex]; + }, +); + export function getRequestingNetworkInfo(state, chainIds) { // If chainIds is undefined, set it to an empty array let processedChainIds = chainIds === undefined ? [] : chainIds; diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 5772f9642805..d6656e481709 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -6,15 +6,14 @@ import { } from '@metamask/keyring-api'; import { deepClone } from '@metamask/snaps-utils'; import { TransactionStatus } from '@metamask/transaction-controller'; -import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { KeyringType } from '../../shared/constants/keyring'; -import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; -import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; import mockState from '../../test/data/mock-state.json'; +import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; import { createMockInternalAccount } from '../../test/jest/mocks'; +import { getProviderConfig } from '../ducks/metamask/metamask'; import { mockNetworkState } from '../../test/stub/networks'; +import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import { selectSwitchedNetworkNeverShowMessage } from '../components/app/toast-master/selectors'; -import { getProviderConfig } from '../ducks/metamask/metamask'; import * as selectors from './selectors'; jest.mock('../../app/scripts/lib/util', () => ({ @@ -79,49 +78,6 @@ describe('Selectors', () => { }); }); - describe('#getSelectedInternalAccount', () => { - it('returns undefined if selectedAccount is undefined', () => { - expect( - selectors.getSelectedInternalAccount({ - metamask: { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }, - }), - ).toBeUndefined(); - }); - - it('returns selectedAccount', () => { - const mockInternalAccount = { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }; - expect( - selectors.getSelectedInternalAccount({ - metamask: { - internalAccounts: { - accounts: { - [mockInternalAccount.id]: mockInternalAccount, - }, - selectedAccount: mockInternalAccount.id, - }, - }, - }), - ).toStrictEqual(mockInternalAccount); - }); - }); - describe('#checkIfMethodIsEnabled', () => { it('returns true if the method is enabled', () => { expect( @@ -157,14 +113,6 @@ describe('Selectors', () => { }); }); - describe('#getInternalAccounts', () => { - it('returns a list of internal accounts', () => { - expect(selectors.getInternalAccounts(mockState)).toStrictEqual( - Object.values(mockState.metamask.internalAccounts.accounts), - ); - }); - }); - describe('#getInternalAccount', () => { it("returns undefined if the account doesn't exist", () => { expect( @@ -974,28 +922,6 @@ describe('Selectors', () => { }); }); - it('returns selected internalAccount', () => { - expect(selectors.getSelectedInternalAccount(mockState)).toStrictEqual({ - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: [ - 'personal_sign', - 'eth_signTransaction', - 'eth_signTypedData_v1', - 'eth_signTypedData_v3', - 'eth_signTypedData_v4', - ], - type: 'eip155:eoa', - }); - }); - it('returns selected account', () => { const account = selectors.getSelectedAccount(mockState); expect(account.balance).toStrictEqual('0x346ba7725f412cbfdb'); @@ -1475,6 +1401,7 @@ describe('Selectors', () => { balance: '0x0', id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', metadata: { + importTime: 0, name: 'Test Account 2', keyring: { type: 'HD Key Tree', @@ -1499,6 +1426,7 @@ describe('Selectors', () => { balance: '0x0', id: '784225f4-d30b-4e77-a900-c8bbce735b88', metadata: { + importTime: 0, name: 'Test Account 3', keyring: { type: 'HD Key Tree', @@ -1522,6 +1450,7 @@ describe('Selectors', () => { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', metadata: { + importTime: 0, name: 'Test Account', keyring: { type: 'HD Key Tree', @@ -1547,6 +1476,7 @@ describe('Selectors', () => { address: '0xc42edfcc21ed14dda456aa0756c153f7985d8813', id: '15e69915-2a1a-4019-93b3-916e11fd432f', metadata: { + importTime: 0, name: 'Ledger Hardware 2', keyring: { type: 'Ledger Hardware', @@ -1574,8 +1504,10 @@ describe('Selectors', () => { keyring: { type: 'Snap Keyring', }, + importTime: 0, name: 'Snap Account 1', snap: { + enabled: true, id: 'snap-id', name: 'snap-name', }, @@ -1596,6 +1528,7 @@ describe('Selectors', () => { { id: '694225f4-d30b-4e77-a900-c8bbce735b42', metadata: { + importTime: 0, name: 'Test Account 4', keyring: { type: 'Custody test', diff --git a/ui/selectors/snaps/accounts.ts b/ui/selectors/snaps/accounts.ts new file mode 100644 index 000000000000..55a30f0c72eb --- /dev/null +++ b/ui/selectors/snaps/accounts.ts @@ -0,0 +1,39 @@ +import { createSelector } from 'reselect'; +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { getAccountName } from '../selectors'; +import { getInternalAccounts } from '../accounts'; +import { createDeepEqualSelector } from '../util'; + +/** + * The Metamask state for the accounts controller. + */ +export type AccountsMetaMaskState = { + metamask: AccountsControllerState; +}; + +/** + * Get the account name for an address. + * + * @param _state - The Metamask state for the accounts controller. + * @param address - The address to get the display name for. + * @returns The account name for the address. + */ +export const getAccountNameFromState = createSelector( + [ + getInternalAccounts, + (_state: AccountsMetaMaskState, address: string) => address, + ], + getAccountName, +); + +/** + * Get the memoized account name for an address. + * + * @param state - The Metamask state for the accounts controller. + * @param address - The address to get the display name for. + * @returns The account name for the address. + */ +export const getMemoizedAccountName = createDeepEqualSelector( + [getAccountNameFromState], + (accountName: string) => accountName, +); diff --git a/ui/selectors/snaps/address-book.ts b/ui/selectors/snaps/address-book.ts new file mode 100644 index 000000000000..e002153d9e61 --- /dev/null +++ b/ui/selectors/snaps/address-book.ts @@ -0,0 +1,80 @@ +import { AddressBookController } from '@metamask/address-book-controller'; +import { createDeepEqualSelector } from '../util'; +import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; + +/** + * The Metamask state for the address book controller. + */ +export type AddressBookMetaMaskState = { + metamask: { + addressBook: AddressBookController['state']['addressBook']; + }; +}; + +/** + * Get the full address book. + * + * @param state - The Metamask state for the address book controller. + * @returns The full address book. + */ +export const getFullAddressBook = (state: AddressBookMetaMaskState) => + state.metamask.addressBook; + +/** + * Get the memoized full address book. + * + * @param state - The Metamask state for the address book controller. + * @returns The full address book. + */ +export const getMemoizedFullAddressBook = createDeepEqualSelector( + [getFullAddressBook], + (addressBook) => addressBook, +); + +/** + * Get the address book for a network. + * + * @param _state - The Metamask state for the address book controller. + * @param chainId - The chain ID to get the address book for. + * @returns The address book for the network. + */ +export const getAddressBookByNetwork = createDeepEqualSelector( + [ + getMemoizedFullAddressBook, + (_state: AddressBookMetaMaskState, chainId: `0x${string}`) => chainId, + ], + (addressBook, chainId) => { + if (!addressBook[chainId]) { + return []; + } + return Object.values(addressBook[chainId]); + }, +); + +/* eslint-disable jsdoc/require-param */ +/* eslint-disable jsdoc/check-param-names */ +/** + * Get an address book entry for an address on a network. + * + * @param state - The Metamask state for the address book controller. + * @param address - The address to get the entry for. + * @param chainId - The chain ID to get the entry for. + * @returns The address book entry for the address on the network. + */ +/* eslint-enable jsdoc/require-param */ +/* eslint-enable jsdoc/check-param-names */ +export const getAddressBookEntryByNetwork = createDeepEqualSelector( + [ + ( + state: AddressBookMetaMaskState, + _address: string, + chainId: `0x${string}`, + ) => getAddressBookByNetwork(state, chainId), + (_state, address) => address, + ], + (addressBook, address) => { + return addressBook.find((contact) => + isEqualCaseInsensitive(contact.address, address), + ); + }, +); diff --git a/ui/selectors/snaps/index.ts b/ui/selectors/snaps/index.ts new file mode 100644 index 000000000000..84a134e4550f --- /dev/null +++ b/ui/selectors/snaps/index.ts @@ -0,0 +1,2 @@ +export * from './address-book'; +export * from './accounts'; diff --git a/ui/selectors/transactions.js b/ui/selectors/transactions.js index 2c5fb8fe4b98..3074fd4bfde4 100644 --- a/ui/selectors/transactions.js +++ b/ui/selectors/transactions.js @@ -13,7 +13,8 @@ import txHelper from '../helpers/utils/tx-helper'; import { SmartTransactionStatus } from '../../shared/constants/transaction'; import { hexToDecimal } from '../../shared/modules/conversion.utils'; import { getProviderConfig } from '../ducks/metamask/metamask'; -import { getCurrentChainId, getSelectedInternalAccount } from './selectors'; +import { getCurrentChainId } from './selectors'; +import { getSelectedInternalAccount } from './accounts'; import { hasPendingApprovals, getApprovalRequestsByType } from './approvals'; import { createDeepEqualSelector, diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 6f8080e516ae..2e31c6c7dd7f 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -49,6 +49,8 @@ export const LOCK_METAMASK = 'LOCK_METAMASK'; // error handling export const DISPLAY_WARNING = 'DISPLAY_WARNING'; export const HIDE_WARNING = 'HIDE_WARNING'; +export const SHOW_SETTINGS_PAGE_ERROR = 'SHOW_SETTINGS_PAGE_ERROR'; +export const HIDE_SETTINGS_PAGE_ERROR = 'HIDE_SETTINGS_PAGE_ERROR'; export const CAPTURE_SINGLE_EXCEPTION = 'CAPTURE_SINGLE_EXCEPTION'; // accounts screen export const SHOW_ACCOUNTS_PAGE = 'SHOW_ACCOUNTS_PAGE'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 1bd16af5a064..11df6a3f3e20 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3089,6 +3089,10 @@ export function setRedesignedConfirmationsEnabled(value: boolean) { return setPreference('redesignedConfirmationsEnabled', value); } +export function setPrivacyMode(value: boolean) { + return setPreference('privacyMode', value, false); +} + export function setRedesignedTransactionsEnabled(value: boolean) { return setPreference('redesignedTransactionsEnabled', value); } @@ -3109,6 +3113,10 @@ export function setTokenSortConfig(value: SortCriteria) { return setPreference('tokenSortConfig', value, false); } +export function setTokenNetworkFilter(value: Record) { + return setPreference('tokenNetworkFilter', value, false); +} + export function setSmartTransactionsPreferenceEnabled( value: boolean, ): ThunkAction { @@ -4517,15 +4525,15 @@ export async function removePollingTokenFromAppState(pollingToken: string) { /** * Informs the CurrencyRateController that the UI requires currency rate polling * - * @param networkClientId - unique identifier for the network client + * @param nativeCurrencies - An array of native currency symbols * @returns polling token that can be used to stop polling */ -export async function currencyRateStartPollingByNetworkClientId( - networkClientId: string, +export async function currencyRateStartPolling( + nativeCurrencies: string[], ): Promise { const pollingToken = await submitRequestToBackground( - 'currencyRateStartPollingByNetworkClientId', - [networkClientId], + 'currencyRateStartPolling', + [{ nativeCurrencies }], ); await addPollingTokenToAppState(pollingToken); return pollingToken; @@ -4547,6 +4555,37 @@ export async function currencyRateStopPollingByPollingToken( await removePollingTokenFromAppState(pollingToken); } +/** + * Informs the TokenRatesController that the UI requires + * token rate polling for the given chain id. + * + * @param chainId - The chain id to poll token rates on. + * @returns polling token that can be used to stop polling + */ +export async function tokenRatesStartPolling(chainId: string): Promise { + const pollingToken = await submitRequestToBackground( + 'tokenRatesStartPolling', + [{ chainId }], + ); + await addPollingTokenToAppState(pollingToken); + return pollingToken; +} + +/** + * Informs the TokenRatesController that the UI no longer + * requires token rate polling for the given chain id. + * + * @param pollingToken - + */ +export async function tokenRatesStopPollingByPollingToken( + pollingToken: string, +) { + await submitRequestToBackground('tokenRatesStopPollingByPollingToken', [ + pollingToken, + ]); + await removePollingTokenFromAppState(pollingToken); +} + /** * Informs the GasFeeController that the UI requires gas fee polling * diff --git a/yarn.lock b/yarn.lock index b2fd10c08c26..a1f4dfe4ea4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1646,13 +1646,13 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.0, @babel/types@npm:^7.13.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.25.9 - resolution: "@babel/types@npm:7.25.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.0, @babel/types@npm:^7.13.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" dependencies: "@babel/helper-string-parser": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10/dd0f2874b10048aa230a5633ab440bbee8c3905f254ef26223b5321ddb824b057b9404d24a87556c6a9f7430198fa6311473778d147ed8ed7845428aee2ebc34 + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e languageName: node linkType: hard @@ -1727,7 +1727,7 @@ __metadata: languageName: node linkType: hard -"@discoveryjs/json-ext@npm:^0.5.3": +"@discoveryjs/json-ext@npm:^0.5.0, @discoveryjs/json-ext@npm:^0.5.3": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" checksum: 10/b95682a852448e8ef50d6f8e3b7ba288aab3fd98a2bafbe46881a3db0c6e7248a2debe9e1ee0d4137c521e4743ca5bbcb1c0765c9d7b3e0ef53231506fec42b4 @@ -4772,9 +4772,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:38.3.0": - version: 38.3.0 - resolution: "@metamask/assets-controllers@npm:38.3.0" +"@metamask/assets-controllers@npm:42.0.0": + version: 42.0.0 + resolution: "@metamask/assets-controllers@npm:42.0.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4782,14 +4782,14 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/polling-controller": "npm:^10.0.1" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/polling-controller": "npm:^12.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" "@types/uuid": "npm:^8.3.0" async-mutex: "npm:^0.5.0" @@ -4804,15 +4804,15 @@ __metadata: "@metamask/accounts-controller": ^18.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 - "@metamask/network-controller": ^21.0.0 + "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/b6e69c9925c50f351b9de1e31cc5d9a4c0ab7cf1abf116c0669611ecb58b3890dd0de53d36bcaaea4f8c45d6ddc2c53eef80c42f93f8f303f1ee9d8df088872b + checksum: 10/64d2bd43139ee5c19bd665b07212cd5d5dd41b457dedde3b5db31442292c4d064dc015011f5f001bb423683675fb20898ff652e91d2339ad1d21cc45fa93487a languageName: node linkType: hard -"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch": - version: 38.3.0 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch::version=38.3.0&hash=e14ff8" +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch": + version: 42.0.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch::version=42.0.0&hash=e14ff8" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4820,14 +4820,14 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/base-controller": "npm:^7.0.1" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/polling-controller": "npm:^10.0.1" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/polling-controller": "npm:^12.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" "@types/uuid": "npm:^8.3.0" async-mutex: "npm:^0.5.0" @@ -4842,9 +4842,9 @@ __metadata: "@metamask/accounts-controller": ^18.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 - "@metamask/network-controller": ^21.0.0 + "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/1f57289a3a2a88f1f16e00a138b30b9a8e4ac894086732a463e6b47d5e984e0a7e05ef2ec345f0e1cd69857669253260d53d4c37b2b3d9b970999602fc01a21c + checksum: 10/9a6727b28f88fd2df3f4b1628dd5d8c2f3e73fd4b9cd090f22d175c2522faa6c6b7e9a93d0ec2b2d123a263c8f4116fbfe97f196b99401b28ac8597f522651eb languageName: node linkType: hard @@ -4882,13 +4882,13 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1": - version: 7.0.1 - resolution: "@metamask/base-controller@npm:7.0.1" +"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1, @metamask/base-controller@npm:^7.0.2": + version: 7.0.2 + resolution: "@metamask/base-controller@npm:7.0.2" dependencies: - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^10.0.0" immer: "npm:^9.0.6" - checksum: 10/774b6d68ac95a5ec187e890d321bede50065f8a6f1ba7b49a19f5971366274054ac0e401548b51d3b014d0bca5d650409fb554dd13ce120e7fb3495b4e8e67b1 + checksum: 10/6f78ec5af840c9947aa8eac6e402df6469600260d613a92196daefd5b072097a176fe5da1c386f2d36853513254b74140d667d817a12880c46f088e18ff3606a languageName: node linkType: hard @@ -4925,20 +4925,21 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0": - version: 11.4.0 - resolution: "@metamask/controller-utils@npm:11.4.0" +"@metamask/controller-utils@npm:^11.0.0, @metamask/controller-utils@npm:^11.0.2, @metamask/controller-utils@npm:^11.1.0, @metamask/controller-utils@npm:^11.2.0, @metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@npm:^11.4.1, @metamask/controller-utils@npm:^11.4.2": + version: 11.4.2 + resolution: "@metamask/controller-utils@npm:11.4.2" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^10.0.0" "@spruceid/siwe-parser": "npm:2.1.0" "@types/bn.js": "npm:^5.1.5" + bignumber.js: "npm:^9.1.2" bn.js: "npm:^5.2.1" eth-ens-namehash: "npm:^2.0.8" fast-deep-equal: "npm:^3.1.3" - checksum: 10/f34d24880eab264bddaa5bef21afaecb206db6978364565d0f7b7a54b1d411f129eb84175041df3be8a66394c2d49e83b6648b5cbde6f34662a60fc553c31458 + checksum: 10/fdae49ee97e7a2a1bb6414011ca59932f8712a768a9c4c43673a2504c9fa9e61d83df53a21ff0506ef6a8cf774704f2df58a6d71385c8786ec5cab4359c051e1 languageName: node linkType: hard @@ -5248,6 +5249,20 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-sig-util@npm:^8.0.0": + version: 8.0.0 + resolution: "@metamask/eth-sig-util@npm:8.0.0" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + "@metamask/abi-utils": "npm:^2.0.4" + "@metamask/utils": "npm:^9.0.0" + "@scure/base": "npm:~1.1.3" + ethereum-cryptography: "npm:^2.1.2" + tweetnacl: "npm:^1.0.3" + checksum: 10/5de92bc59df31bcf417ecbdfd2b47f15c21b29454f45108513c55d9c005b7cb51373e9d254bd97533603ab7c7758fdf8fc5159612f366b05f92ebe5beb6d75d8 + languageName: node + linkType: hard + "@metamask/eth-simple-keyring@npm:^6.0.5": version: 6.0.5 resolution: "@metamask/eth-simple-keyring@npm:6.0.5" @@ -5475,14 +5490,14 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^10.0.0": - version: 10.0.0 - resolution: "@metamask/json-rpc-engine@npm:10.0.0" +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/json-rpc-engine@npm:10.0.1" dependencies: - "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.1.0" - checksum: 10/2c401a4a64392aeb11c4f7ca8d7b458ba1106cff1e0b3dba8b3e0cc90e82f8c55ac2dc9fdfcd914b289e3298fb726d637cf21382336dde2c207cf76129ce5eab + "@metamask/utils": "npm:^10.0.0" + checksum: 10/15a8eeab9af39b9ed87311da728e81169484ace733a8ef9fc469bd887654e37afa19f9e5228246dc80daad3fbf9b16067e73b2969d37d44acf5bc6ffa2c70082 languageName: node linkType: hard @@ -5892,6 +5907,22 @@ __metadata: languageName: node linkType: hard +"@metamask/polling-controller@npm:^12.0.1": + version: 12.0.1 + resolution: "@metamask/polling-controller@npm:12.0.1" + dependencies: + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/utils": "npm:^10.0.0" + "@types/uuid": "npm:^8.3.0" + fast-json-stable-stringify: "npm:^2.1.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/network-controller": ^22.0.0 + checksum: 10/eac9ed2fcc9697a2aa55e9746d4eac8d762dd6948b00d77cd2d4894b8c3e1a8e6ed5d0df4d01a69d9a7e2b3c09d9d7c1ffc6f9504023388dd7452d45b5d87065 + languageName: node + linkType: hard + "@metamask/polling-controller@npm:^8.0.0": version: 8.0.0 resolution: "@metamask/polling-controller@npm:8.0.0" @@ -6021,20 +6052,20 @@ __metadata: languageName: node linkType: hard -"@metamask/queued-request-controller@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/queued-request-controller@npm:2.0.0" +"@metamask/queued-request-controller@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/queued-request-controller@npm:7.0.0" dependencies: - "@metamask/base-controller": "npm:^6.0.0" - "@metamask/controller-utils": "npm:^11.0.0" - "@metamask/json-rpc-engine": "npm:^9.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/swappable-obj-proxy": "npm:^2.2.0" - "@metamask/utils": "npm:^8.3.0" + "@metamask/utils": "npm:^10.0.0" peerDependencies: - "@metamask/network-controller": ^19.0.0 - "@metamask/selected-network-controller": ^15.0.0 - checksum: 10/b618fa05465a52e5b689d932d99b47552b5987a9141d58260966611f1057190132f14b1a2123c48399f218fc57c577e1c86375e8ee2b43871cdc597fbaeedb7a + "@metamask/network-controller": ^22.0.0 + "@metamask/selected-network-controller": ^19.0.0 + checksum: 10/69118c11e3faecdbec7c9f02f4ecec4734ce0950115bfac0cdd4338309898690ae3187bcef1cc4f75f54c5c02eff07d80286d3ef29088a665039c13cb50bef88 languageName: node linkType: hard @@ -6059,13 +6090,13 @@ __metadata: languageName: node linkType: hard -"@metamask/rpc-errors@npm:^7.0.0": - version: 7.0.0 - resolution: "@metamask/rpc-errors@npm:7.0.0" +"@metamask/rpc-errors@npm:^7.0.0, @metamask/rpc-errors@npm:^7.0.1": + version: 7.0.1 + resolution: "@metamask/rpc-errors@npm:7.0.1" dependencies: - "@metamask/utils": "npm:^9.0.0" + "@metamask/utils": "npm:^10.0.0" fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd + checksum: 10/819708b4a7d9695ee67fd867d8f94bb5a273b479a242b17bd53c83d1fceec421fc42928f0bb340f4f138ec803dd82ec9659ce7b09a86aedad6a81d5a39ec5c35 languageName: node linkType: hard @@ -6101,14 +6132,14 @@ __metadata: languageName: node linkType: hard -"@metamask/signature-controller@npm:^20.0.0": - version: 20.0.0 - resolution: "@metamask/signature-controller@npm:20.0.0" +"@metamask/signature-controller@npm:^21.0.0": + version: 21.0.0 + resolution: "@metamask/signature-controller@npm:21.0.0" dependencies: - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.3.0" - "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/eth-sig-util": "npm:^8.0.0" + "@metamask/utils": "npm:^10.0.0" jsonschema: "npm:^1.2.4" lodash: "npm:^4.17.21" uuid: "npm:^8.3.2" @@ -6116,7 +6147,8 @@ __metadata: "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 "@metamask/logging-controller": ^6.0.0 - checksum: 10/5647e362b4478d9cdb9f04027d7bad950efbe310496fc0347a92649a084bb92fc92a7fc5f911f8835e0d6b4e7ed6cf572594a79a57a31240948b87dd2267cdf8 + "@metamask/network-controller": ^22.0.0 + checksum: 10/4c1b1cbf909004099adb3f0d2b01c8fe640ae9a13a8e53ffbcf05c7a1a23384f6077b96b845c22c4edf3bceaaff2a705769d4623f37affac7e429ab0dae06912 languageName: node linkType: hard @@ -6397,9 +6429,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^37.3.0": - version: 37.3.0 - resolution: "@metamask/transaction-controller@npm:37.3.0" +"@metamask/transaction-controller@npm:^38.1.0": + version: 38.1.0 + resolution: "@metamask/transaction-controller@npm:38.1.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6407,13 +6439,13 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.1" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/utils": "npm:^10.0.0" async-mutex: "npm:^0.5.0" bn.js: "npm:^5.2.1" eth-method-registry: "npm:^4.0.0" @@ -6424,9 +6456,9 @@ __metadata: "@babel/runtime": ^7.23.9 "@metamask/accounts-controller": ^18.0.0 "@metamask/approval-controller": ^7.0.0 - "@metamask/gas-fee-controller": ^20.0.0 - "@metamask/network-controller": ^21.0.0 - checksum: 10/314a46bdaf1a4c68fe232591d28f3f978d7ed17f19dbaa2e3cbcbc4d28d4f7fc4581d7f88446d31ced2176f4f2abf1022ae39a296cb884fd5a083181c562ee2c + "@metamask/gas-fee-controller": ^22.0.0 + "@metamask/network-controller": ^22.0.0 + checksum: 10/c1bdca52bbbce42a76ec9c640197534ec6c223b0f5d5815acfa53490dc1175850ea9aeeb6ae3c5ec34218f0bdbbbeb3e8731e2552aa9411e3ed7798a5dea8ab5 languageName: node linkType: hard @@ -6460,6 +6492,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/utils@npm:10.0.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/9c2e6421f685d8a45145b6026a6f9fd0701eb5a2e8490fc6d18e64c103d5a62097f301cbc797790da52ceb5853bd9f65845c934b00299e69e5e6736c52b32f0f + languageName: node + linkType: hard + "@metamask/utils@npm:^8.1.0, @metamask/utils@npm:^8.2.0, @metamask/utils@npm:^8.3.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" @@ -7018,6 +7067,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.28 + resolution: "@polka/url@npm:1.0.0-next.28" + checksum: 10/7402aaf1de781d0eb0870d50cbcd394f949aee11b38a267a5c3b4e3cfee117e920693e6e93ce24c87ae2d477a59634f39d9edde8e86471cae756839b07c79af7 + languageName: node + linkType: hard + "@popperjs/core@npm:^2.4.0": version: 2.9.2 resolution: "@popperjs/core@npm:2.9.2" @@ -7890,6 +7946,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/is@npm:^0.14.0": + version: 0.14.0 + resolution: "@sindresorhus/is@npm:0.14.0" + checksum: 10/789cd128f0b43e158e657c4505539c8997905fcb5c06d750b7df778cab2b6887bc1eb8878026a20d84524528786ef69fc3d12a964ae56a478a87bcfc7f8272f3 + languageName: node + linkType: hard + "@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.2.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" @@ -9060,6 +9123,133 @@ __metadata: languageName: node linkType: hard +"@svgr/babel-plugin-add-jsx-attribute@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/cab83832830a57735329ed68f67c03b57ca21fa037b0134847b0c5c0ef4beca89956d7dacfbf7b2a10fd901e7009e877512086db2ee918b8c69aee7742ae32c0 + languageName: node + linkType: hard + +"@svgr/babel-plugin-remove-jsx-attribute@npm:*": + version: 8.0.0 + resolution: "@svgr/babel-plugin-remove-jsx-attribute@npm:8.0.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/ff992893c6c4ac802713ba3a97c13be34e62e6d981c813af40daabcd676df68a72a61bd1e692bb1eda3587f1b1d700ea462222ae2153bb0f46886632d4f88d08 + languageName: node + linkType: hard + +"@svgr/babel-plugin-remove-jsx-empty-expression@npm:*": + version: 8.0.0 + resolution: "@svgr/babel-plugin-remove-jsx-empty-expression@npm:8.0.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0fb691b63a21bac00da3aa2dccec50d0d5a5b347ff408d60803b84410d8af168f2656e4ba1ee1f24dab0ae4e4af77901f2928752bb0434c1f6788133ec599ec8 + languageName: node + linkType: hard + +"@svgr/babel-plugin-replace-jsx-attribute-value@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-replace-jsx-attribute-value@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b7d2125758e766e1ebd14b92216b800bdc976959bc696dbfa1e28682919147c1df4bb8b1b5fd037d7a83026e27e681fea3b8d3741af8d3cf4c9dfa3d412125df + languageName: node + linkType: hard + +"@svgr/babel-plugin-svg-dynamic-title@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-svg-dynamic-title@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0fd42ebf127ae9163ef341e84972daa99bdcb9e6ed3f83aabd95ee173fddc43e40e02fa847fbc0a1058cf5549f72b7960a2c5e22c3e4ac18f7e3ac81277852ae + languageName: node + linkType: hard + +"@svgr/babel-plugin-svg-em-dimensions@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-svg-em-dimensions@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/c1550ee9f548526fa66fd171e3ffb5696bfc4e4cd108a631d39db492c7410dc10bba4eb5a190e9df824bf806130ccc586ae7d2e43c547e6a4f93bbb29a18f344 + languageName: node + linkType: hard + +"@svgr/babel-plugin-transform-react-native-svg@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-transform-react-native-svg@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/4c924af22b948b812629e80efb90ad1ec8faae26a232d8ca8a06b46b53e966a2c415a57806a3ff0ea806a622612e546422719b69ec6839717a7755dac19171d9 + languageName: node + linkType: hard + +"@svgr/babel-plugin-transform-svg-component@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-transform-svg-component@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a4ddd3cf8b1a7a0542ff2c6a3eb7a75d6f79a86a62210306d94fb05e59699bb5da4ddde9ce98ef477b9cd528007fb728dc4d388d413b3aa25f48ed92b1f0a1c1 + languageName: node + linkType: hard + +"@svgr/babel-preset@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-preset@npm:6.5.1" + dependencies: + "@svgr/babel-plugin-add-jsx-attribute": "npm:^6.5.1" + "@svgr/babel-plugin-remove-jsx-attribute": "npm:*" + "@svgr/babel-plugin-remove-jsx-empty-expression": "npm:*" + "@svgr/babel-plugin-replace-jsx-attribute-value": "npm:^6.5.1" + "@svgr/babel-plugin-svg-dynamic-title": "npm:^6.5.1" + "@svgr/babel-plugin-svg-em-dimensions": "npm:^6.5.1" + "@svgr/babel-plugin-transform-react-native-svg": "npm:^6.5.1" + "@svgr/babel-plugin-transform-svg-component": "npm:^6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9f124be39a8e64f909162f925b3a63ddaa5a342a5e24fc0b7f7d9d4d7f7e3b916596c754fb557dc259928399cad5366a27cb231627a0d2dcc4b13ac521cf05af + languageName: node + linkType: hard + +"@svgr/core@npm:^6.2.1": + version: 6.5.1 + resolution: "@svgr/core@npm:6.5.1" + dependencies: + "@babel/core": "npm:^7.19.6" + "@svgr/babel-preset": "npm:^6.5.1" + "@svgr/plugin-jsx": "npm:^6.5.1" + camelcase: "npm:^6.2.0" + cosmiconfig: "npm:^7.0.1" + checksum: 10/0aa3078eefb969d93fb5639c2d64c8868cf65134f0e36a1733dc595acc990081cbad62295e34b860150ce6baa21516d71410c5527579a1a0950cdc35a765873a + languageName: node + linkType: hard + +"@svgr/hast-util-to-babel-ast@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/hast-util-to-babel-ast@npm:6.5.1" + dependencies: + "@babel/types": "npm:^7.20.0" + entities: "npm:^4.4.0" + checksum: 10/0410c6e5bf98fe31729ab1785642b915e7645e65c7ee5b2dd292a4603f8a1377402b95237c550b10dbdcc0bf084df1546ac7e98004d1fe5982cb8508147b47bb + languageName: node + linkType: hard + +"@svgr/plugin-jsx@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/plugin-jsx@npm:6.5.1" + dependencies: + "@babel/core": "npm:^7.19.6" + "@svgr/babel-preset": "npm:^6.5.1" + "@svgr/hast-util-to-babel-ast": "npm:^6.5.1" + svg-parser: "npm:^2.0.4" + peerDependencies: + "@svgr/core": ^6.0.0 + checksum: 10/42f22847a6bdf930514d7bedd3c5e1fd8d53eb3594779f9db16cb94c762425907c375cd8ec789114e100a4d38068aca6c7ab5efea4c612fba63f0630c44cc859 + languageName: node + linkType: hard + "@swc/core-darwin-arm64@npm:1.4.11": version: 1.4.11 resolution: "@swc/core-darwin-arm64@npm:1.4.11" @@ -9338,6 +9528,15 @@ __metadata: languageName: node linkType: hard +"@szmarczak/http-timer@npm:^1.1.2": + version: 1.1.2 + resolution: "@szmarczak/http-timer@npm:1.1.2" + dependencies: + defer-to-connect: "npm:^1.0.1" + checksum: 10/9b63853bd53bff72c4990ebc9cd3f625bbab757247099af172564da6649a27a1d41b1a70cd849dd65b2a078300029c1c80bf3079e6a91e285da7b259eb147146 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -11545,6 +11744,39 @@ __metadata: languageName: node linkType: hard +"@webpack-cli/configtest@npm:^2.1.1": + version: 2.1.1 + resolution: "@webpack-cli/configtest@npm:2.1.1" + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + checksum: 10/9f9f9145c2d05471fc83d426db1df85cf49f329836b0c4b9f46b6948bed4b013464c00622b136d2a0a26993ce2306976682592245b08ee717500b1db45009a72 + languageName: node + linkType: hard + +"@webpack-cli/info@npm:^2.0.2": + version: 2.0.2 + resolution: "@webpack-cli/info@npm:2.0.2" + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + checksum: 10/8f9a178afca5c82e113aed1efa552d64ee5ae4fdff63fe747c096a981ec74f18a5d07bd6e89bbe6715c3e57d96eea024a410e58977169489fe1df044c10dd94e + languageName: node + linkType: hard + +"@webpack-cli/serve@npm:^2.0.5": + version: 2.0.5 + resolution: "@webpack-cli/serve@npm:2.0.5" + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + peerDependenciesMeta: + webpack-dev-server: + optional: true + checksum: 10/20424e5c1e664e4d7ab11facee7033bb729f6acd86493138069532934c1299c1426da72942822dedb00caca8fc60cc8aec1626e610ee0e8a9679e3614f555860 + languageName: node + linkType: hard + "@welldone-software/why-did-you-render@npm:^8.0.3": version: 8.0.3 resolution: "@welldone-software/why-did-you-render@npm:8.0.3" @@ -12112,6 +12344,15 @@ __metadata: languageName: node linkType: hard +"ansi-align@npm:^3.0.0": + version: 3.0.1 + resolution: "ansi-align@npm:3.0.1" + dependencies: + string-width: "npm:^4.1.0" + checksum: 10/4c7e8b6a10eaf18874ecee964b5db62ac86d0b9266ad4987b3a1efcb5d11a9e12c881ee40d14951833135a8966f10a3efe43f9c78286a6e632f53d85ad28b9c0 + languageName: node + linkType: hard + "ansi-colors@npm:1.1.0, ansi-colors@npm:^1.0.1": version: 1.1.0 resolution: "ansi-colors@npm:1.1.0" @@ -13547,6 +13788,22 @@ __metadata: languageName: node linkType: hard +"boxen@npm:^5.0.0": + version: 5.1.2 + resolution: "boxen@npm:5.1.2" + dependencies: + ansi-align: "npm:^3.0.0" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.1.0" + cli-boxes: "npm:^2.2.1" + string-width: "npm:^4.2.2" + type-fest: "npm:^0.20.2" + widest-line: "npm:^3.1.0" + wrap-ansi: "npm:^7.0.0" + checksum: 10/bc3d3d88d77dc8cabb0811844acdbd4805e8ca8011222345330817737042bf6f86d93eb74a3f7e0cab634e64ef69db03cf52b480761ed90a965de0c8ff1bea8c + languageName: node + linkType: hard + "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -14138,6 +14395,21 @@ __metadata: languageName: node linkType: hard +"cacheable-request@npm:^6.0.0": + version: 6.1.0 + resolution: "cacheable-request@npm:6.1.0" + dependencies: + clone-response: "npm:^1.0.2" + get-stream: "npm:^5.1.0" + http-cache-semantics: "npm:^4.0.0" + keyv: "npm:^3.0.0" + lowercase-keys: "npm:^2.0.0" + normalize-url: "npm:^4.1.0" + responselike: "npm:^1.0.2" + checksum: 10/804f6c377ce6fef31c584babde31d55c69305569058ad95c24a41bb7b33d0ea188d388467a9da6cb340e95a3a1f8a94e1f3a709fef5eaf9c6b88e62448fa29be + languageName: node + linkType: hard + "cacheable-request@npm:^7.0.2": version: 7.0.2 resolution: "cacheable-request@npm:7.0.2" @@ -14241,7 +14513,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0": +"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 10/8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -14516,6 +14788,13 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:^2.0.0": + version: 2.0.0 + resolution: "ci-info@npm:2.0.0" + checksum: 10/3b374666a85ea3ca43fa49aa3a048d21c9b475c96eb13c133505d2324e7ae5efd6a454f41efe46a152269e9b6a00c9edbe63ec7fa1921957165aae16625acd67 + languageName: node + linkType: hard + "ci-info@npm:^3.2.0": version: 3.3.2 resolution: "ci-info@npm:3.3.2" @@ -14608,6 +14887,13 @@ __metadata: languageName: node linkType: hard +"cli-boxes@npm:^2.2.1": + version: 2.2.1 + resolution: "cli-boxes@npm:2.2.1" + checksum: 10/be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 + languageName: node + linkType: hard + "cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -14914,7 +15200,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10, colorette@npm:^2.0.19": +"colorette@npm:^2.0.10, colorette@npm:^2.0.14, colorette@npm:^2.0.19": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f @@ -15015,7 +15301,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^9.0.0, commander@npm:^9.4.0": +"commander@npm:^9.0.0, commander@npm:^9.2.0, commander@npm:^9.4.0": version: 9.5.0 resolution: "commander@npm:9.5.0" checksum: 10/41c49b3d0f94a1fbeb0463c85b13f15aa15a9e0b4d5e10a49c0a1d58d4489b549d62262b052ae0aa6cfda53299bee487bfe337825df15e342114dde543f82906 @@ -15162,6 +15448,20 @@ __metadata: languageName: node linkType: hard +"configstore@npm:^5.0.1": + version: 5.0.1 + resolution: "configstore@npm:5.0.1" + dependencies: + dot-prop: "npm:^5.2.0" + graceful-fs: "npm:^4.1.2" + make-dir: "npm:^3.0.0" + unique-string: "npm:^2.0.0" + write-file-atomic: "npm:^3.0.0" + xdg-basedir: "npm:^4.0.0" + checksum: 10/60ef65d493b63f96e14b11ba7ec072fdbf3d40110a94fb7199d1c287761bdea5c5244e76b2596325f30c1b652213aa75de96ea20afd4a5f82065e61ea090988e + languageName: node + linkType: hard + "connect-history-api-fallback@npm:^2.0.0": version: 2.0.0 resolution: "connect-history-api-fallback@npm:2.0.0" @@ -16023,6 +16323,15 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^3.3.0": + version: 3.3.0 + resolution: "decompress-response@npm:3.3.0" + dependencies: + mimic-response: "npm:^1.0.0" + checksum: 10/952552ac3bd7de2fc18015086b09468645c9638d98a551305e485230ada278c039c91116e946d07894b39ee53c0f0d5b6473f25a224029344354513b412d7380 + languageName: node + linkType: hard + "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -16182,6 +16491,13 @@ __metadata: languageName: node linkType: hard +"defer-to-connect@npm:^1.0.1": + version: 1.1.3 + resolution: "defer-to-connect@npm:1.1.3" + checksum: 10/9491b301dcfa04956f989481ba7a43c2231044206269eb4ab64a52d6639ee15b1252262a789eb4239fb46ab63e44d4e408641bae8e0793d640aee55398cb3930 + languageName: node + linkType: hard + "defer-to-connect@npm:^2.0.0": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" @@ -16914,6 +17230,15 @@ __metadata: languageName: node linkType: hard +"dot-prop@npm:^5.2.0": + version: 5.3.0 + resolution: "dot-prop@npm:5.3.0" + dependencies: + is-obj: "npm:^2.0.0" + checksum: 10/33b2561617bd5c73cf9305368ba4638871c5dbf9c8100c8335acd2e2d590a81ec0e75c11cfaea5cc3cf8c2f668cad4beddb52c11856d0c9e666348eee1baf57a + languageName: node + linkType: hard + "dot-prop@npm:^6.0.1": version: 6.0.1 resolution: "dot-prop@npm:6.0.1" @@ -17650,6 +17975,13 @@ __metadata: languageName: node linkType: hard +"escape-goat@npm:^2.0.0": + version: 2.1.1 + resolution: "escape-goat@npm:2.1.1" + checksum: 10/ce05c70c20dd7007b60d2d644b625da5412325fdb57acf671ba06cb2ab3cd6789e2087026921a05b665b0a03fadee2955e7fc0b9a67da15a6551a980b260eba7 + languageName: node + linkType: hard + "escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -19012,7 +19344,7 @@ __metadata: languageName: node linkType: hard -"fastest-levenshtein@npm:^1.0.16": +"fastest-levenshtein@npm:^1.0.12, fastest-levenshtein@npm:^1.0.16": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" checksum: 10/ee85d33b5cef592033f70e1c13ae8624055950b4eb832435099cd56aa313d7f251b873bedbc06a517adfaff7b31756d139535991e2406967438e03a1bf1b008e @@ -19274,6 +19606,16 @@ __metadata: languageName: node linkType: hard +"find-node-modules@npm:^2.1.3": + version: 2.1.3 + resolution: "find-node-modules@npm:2.1.3" + dependencies: + findup-sync: "npm:^4.0.0" + merge: "npm:^2.1.1" + checksum: 10/4b8a194ffd56ccf1a1033de35e2ee8209869b05cce68ff7c4ab0dbf04e63fd7196283383eee4c84596c7b311755b2836815209d558234cadc330a87881e5a3f4 + languageName: node + linkType: hard + "find-pkg@npm:^0.1.2": version: 0.1.2 resolution: "find-pkg@npm:0.1.2" @@ -19375,6 +19717,18 @@ __metadata: languageName: node linkType: hard +"findup-sync@npm:^4.0.0": + version: 4.0.0 + resolution: "findup-sync@npm:4.0.0" + dependencies: + detect-file: "npm:^1.0.0" + is-glob: "npm:^4.0.0" + micromatch: "npm:^4.0.2" + resolve-dir: "npm:^1.0.1" + checksum: 10/94131e1107ad63790ed00c4c39ca131a93ea602607bd97afeffd92b69a9a63cf2c6f57d6db88cb753fe748ac7fde79e1e76768ff784247026b7c5ebf23ede3a0 + languageName: node + linkType: hard + "fined@npm:^1.0.1": version: 1.1.0 resolution: "fined@npm:1.1.0" @@ -20048,6 +20402,15 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^4.1.0": + version: 4.1.0 + resolution: "get-stream@npm:4.1.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 10/12673e8aebc79767d187b203e5bfabb8266304037815d3bcc63b6f8c67c6d4ad0d98d4d4528bcdc1cbea68f1dd91bcbd87827aa3cdcfa9c5fa4a4644716d72c2 + languageName: node + linkType: hard + "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0, get-stream@npm:^5.2.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -20318,6 +20681,15 @@ __metadata: languageName: node linkType: hard +"global-dirs@npm:^3.0.0": + version: 3.0.1 + resolution: "global-dirs@npm:3.0.1" + dependencies: + ini: "npm:2.0.0" + checksum: 10/70147b80261601fd40ac02a104581432325c1c47329706acd773f3a6ce99bb36d1d996038c85ccacd482ad22258ec233c586b6a91535b1a116b89663d49d6438 + languageName: node + linkType: hard + "global-modules@npm:^0.2.3": version: 0.2.3 resolution: "global-modules@npm:0.2.3" @@ -20534,6 +20906,25 @@ __metadata: languageName: node linkType: hard +"got@npm:^9.6.0": + version: 9.6.0 + resolution: "got@npm:9.6.0" + dependencies: + "@sindresorhus/is": "npm:^0.14.0" + "@szmarczak/http-timer": "npm:^1.1.2" + cacheable-request: "npm:^6.0.0" + decompress-response: "npm:^3.3.0" + duplexer3: "npm:^0.1.4" + get-stream: "npm:^4.1.0" + lowercase-keys: "npm:^1.0.1" + mimic-response: "npm:^1.0.1" + p-cancelable: "npm:^1.0.0" + to-readable-stream: "npm:^1.0.0" + url-parse-lax: "npm:^3.0.0" + checksum: 10/fae3273b44392b6b1d88071d04ea984784e63dbf8ba3f70b04cb7edda53c7668ee17288ac46af507a9f2aa60c183c5ea1732339141d253dda3eb19f92985c771 + languageName: node + linkType: hard + "graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -20985,6 +21376,13 @@ __metadata: languageName: node linkType: hard +"has-yarn@npm:^2.1.0": + version: 2.1.0 + resolution: "has-yarn@npm:2.1.0" + checksum: 10/5eb1d0bb8518103d7da24532bdbc7124ffc6d367b5d3c10840b508116f2f1bcbcf10fd3ba843ff6e2e991bdf9969fd862d42b2ed58aade88343326c950b7e7f7 + languageName: node + linkType: hard + "has@npm:^1.0.0, has@npm:^1.0.3": version: 1.0.3 resolution: "has@npm:1.0.3" @@ -21785,6 +22183,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:2.0.0": + version: 2.0.0 + resolution: "ini@npm:2.0.0" + checksum: 10/04e24ba05c4f6947e15560824e153b4610bceea2f5a3ab68651d221a4aab3c77d4e3e90a917ebc8bf5ad71a30a8575de56c39d6b4c4b1375a28016b9f3625f9d + languageName: node + linkType: hard + "ini@npm:^1.3.4, ini@npm:^1.3.5, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" @@ -21869,6 +22274,13 @@ __metadata: languageName: node linkType: hard +"interpret@npm:^3.1.1": + version: 3.1.1 + resolution: "interpret@npm:3.1.1" + checksum: 10/bc9e11126949c4e6ff49b0b819e923a9adc8e8bf3f9d4f2d782de6d5f592774f6fee4457c10bd08c6a2146b4baee460ccb242c99e5397defa9c846af0d00505a + languageName: node + linkType: hard + "invariant@npm:2.2.4, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -22078,6 +22490,17 @@ __metadata: languageName: node linkType: hard +"is-ci@npm:^2.0.0": + version: 2.0.0 + resolution: "is-ci@npm:2.0.0" + dependencies: + ci-info: "npm:^2.0.0" + bin: + is-ci: bin.js + checksum: 10/77b869057510f3efa439bbb36e9be429d53b3f51abd4776eeea79ab3b221337fe1753d1e50058a9e2c650d38246108beffb15ccfd443929d77748d8c0cc90144 + languageName: node + linkType: hard + "is-core-module@npm:^2.12.1, is-core-module@npm:^2.13.0, is-core-module@npm:^2.4.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": version: 2.13.1 resolution: "is-core-module@npm:2.13.1" @@ -22345,6 +22768,16 @@ __metadata: languageName: node linkType: hard +"is-installed-globally@npm:^0.4.0": + version: 0.4.0 + resolution: "is-installed-globally@npm:0.4.0" + dependencies: + global-dirs: "npm:^3.0.0" + is-path-inside: "npm:^3.0.2" + checksum: 10/5294d21c82cb9beedd693ce1dfb12117c4db36d6e35edc9dc6bf06cb300d23c96520d1bfb063386b054268ae3d7255c3f09393b52218cc26ace99b217bf37c93 + languageName: node + linkType: hard + "is-interactive@npm:^1.0.0": version: 1.0.0 resolution: "is-interactive@npm:1.0.0" @@ -22411,6 +22844,13 @@ __metadata: languageName: node linkType: hard +"is-npm@npm:^5.0.0": + version: 5.0.0 + resolution: "is-npm@npm:5.0.0" + checksum: 10/9baff02b0c69a3d3c79b162cb2f9e67fb40ef6d172c16601b2e2471c21e9a4fa1fc9885a308d7bc6f3a3cd2a324c27fa0bf284c133c3349bb22571ab70d041cc + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -22811,6 +23251,13 @@ __metadata: languageName: node linkType: hard +"is-yarn-global@npm:^0.3.0": + version: 0.3.0 + resolution: "is-yarn-global@npm:0.3.0" + checksum: 10/bca013d65fee2862024c9fbb3ba13720ffca2fe750095174c1c80922fdda16402b5c233f5ac9e265bc12ecb5446e7b7f519a32d9541788f01d4d44e24d2bf481 + languageName: node + linkType: hard + "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -23407,6 +23854,31 @@ __metadata: languageName: node linkType: hard +"jest-preview@npm:^0.3.1": + version: 0.3.1 + resolution: "jest-preview@npm:0.3.1" + dependencies: + "@svgr/core": "npm:^6.2.1" + camelcase: "npm:^6.3.0" + chalk: "npm:^4.1.2" + chokidar: "npm:^3.5.3" + commander: "npm:^9.2.0" + connect: "npm:^3.7.0" + find-node-modules: "npm:^2.1.3" + open: "npm:^8.4.0" + postcss-import: "npm:^14.1.0" + postcss-load-config: "npm:^4.0.1" + sirv: "npm:^2.0.2" + slash: "npm:^3.0.0" + string-hash: "npm:^1.1.3" + update-notifier: "npm:^5.1.0" + ws: "npm:^8.5.0" + bin: + jest-preview: cli/index.js + checksum: 10/25a68ee58af86081e47a6923356b4ab10760cb2781fb0565774dfa2440bee9993f32d69818600f12d30f69ca2dfd2f354d6758f7882756cb1d100caf5cc9d455 + languageName: node + linkType: hard + "jest-process-manager@npm:^0.3.1": version: 0.3.1 resolution: "jest-process-manager@npm:0.3.1" @@ -23938,6 +24410,13 @@ __metadata: languageName: node linkType: hard +"json-buffer@npm:3.0.0": + version: 3.0.0 + resolution: "json-buffer@npm:3.0.0" + checksum: 10/6e364585600598c42f1cc85d1305569aeb1a6a13e7c67960f17b403f087e2700104ec8e49fc681ab6d6278ee4d132ac033f2625c22a9777ed9b83b403b40f23e + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -24362,6 +24841,15 @@ __metadata: languageName: node linkType: hard +"keyv@npm:^3.0.0": + version: 3.1.0 + resolution: "keyv@npm:3.1.0" + dependencies: + json-buffer: "npm:3.0.0" + checksum: 10/6de272b3f78975a9a0b12259953c09d5bbe9de9acfd845471ebd758928b523f70563462f0c16a866fe9b447ff5bdebda72c62bc23734eb72cd1fb8f1d7076843 + languageName: node + linkType: hard + "kind-of@npm:^2.0.1": version: 2.0.1 resolution: "kind-of@npm:2.0.1" @@ -24518,6 +25006,15 @@ __metadata: languageName: node linkType: hard +"latest-version@npm:^5.1.0": + version: 5.1.0 + resolution: "latest-version@npm:5.1.0" + dependencies: + package-json: "npm:^6.3.0" + checksum: 10/fbc72b071eb66c40f652441fd783a9cca62f08bf42433651937f078cd9ef94bf728ec7743992777826e4e89305aef24f234b515e6030503a2cbee7fc9bdc2c0f + languageName: node + linkType: hard + "launch-editor@npm:^2.6.1": version: 2.6.1 resolution: "launch-editor@npm:2.6.1" @@ -24813,6 +25310,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:^3.0.0": + version: 3.1.2 + resolution: "lilconfig@npm:3.1.2" + checksum: 10/8058403850cfad76d6041b23db23f730e52b6c17a8c28d87b90766639ca0ee40c748a3e85c2d7bd133d572efabff166c4b015e5d25e01fd666cb4b13cfada7f0 + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.1.6 resolution: "lines-and-columns@npm:1.1.6" @@ -25153,7 +25657,7 @@ __metadata: languageName: node linkType: hard -"lowercase-keys@npm:^1.0.0": +"lowercase-keys@npm:^1.0.0, lowercase-keys@npm:^1.0.1": version: 1.0.1 resolution: "lowercase-keys@npm:1.0.1" checksum: 10/12ba64572dc25ae9ee30d37a11f3a91aea046c1b6b905fdf8ac77e2f268f153ed36e60d39cb3bfa47a89f31d981dae9a8cc9915124a56fe51ff01ed6e8bb68fa @@ -25824,6 +26328,13 @@ __metadata: languageName: node linkType: hard +"merge@npm:^2.1.1": + version: 2.1.1 + resolution: "merge@npm:2.1.1" + checksum: 10/1875521a8e429ba8d82c6d24bf3f229b4b64a348873c41a1245851b422c0caa7fbeb958118c24fbfcbb71e416a29924b3b1c4518911529db175f49eb5bcb5e62 + languageName: node + linkType: hard + "mersenne-twister@npm:^1.1.0": version: 1.1.0 resolution: "mersenne-twister@npm:1.1.0" @@ -25886,14 +26397,14 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.8.2" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" - "@metamask/controller-utils": "npm:^11.2.0" + "@metamask/controller-utils": "npm:^11.4.0" "@metamask/design-tokens": "npm:^4.0.0" "@metamask/ens-controller": "npm:^13.0.0" "@metamask/ens-resolver-snap": "npm:^0.1.2" @@ -25937,19 +26448,20 @@ __metadata: "@metamask/permission-log-controller": "npm:^2.0.1" "@metamask/phishing-controller": "npm:^12.3.0" "@metamask/phishing-warning": "npm:^4.1.0" + "@metamask/polling-controller": "npm:^10.0.1" "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.35.1" "@metamask/preferences-controller": "npm:^13.0.2" "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" - "@metamask/queued-request-controller": "npm:^2.0.0" + "@metamask/queued-request-controller": "npm:^7.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^18.0.2" - "@metamask/signature-controller": "npm:^20.0.0" + "@metamask/signature-controller": "npm:^21.0.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.11.1" "@metamask/snaps-execution-environments": "npm:^6.9.1" @@ -25958,7 +26470,7 @@ __metadata: "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.7.0" - "@metamask/transaction-controller": "npm:^37.3.0" + "@metamask/transaction-controller": "npm:^38.1.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^9.3.0" "@ngraveio/bc-ur": "npm:^1.1.12" @@ -26137,6 +26649,7 @@ __metadata: jest-canvas-mock: "npm:^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-junit: "npm:^14.0.1" + jest-preview: "npm:^0.3.1" jsdom: "npm:^16.7.0" json-schema-to-ts: "npm:^3.0.1" koa: "npm:^2.7.0" @@ -26152,6 +26665,7 @@ __metadata: loose-envify: "npm:^1.4.0" lottie-web: "npm:^5.12.2" luxon: "npm:^3.2.1" + mini-css-extract-plugin: "npm:^2.9.1" mocha: "npm:^10.2.0" mocha-junit-reporter: "npm:^2.2.1" mockttp: "npm:^3.10.1" @@ -26246,6 +26760,7 @@ __metadata: web3-stream-provider: "npm:^5.0.0" webextension-polyfill: "npm:^0.8.0" webpack: "npm:^5.91.0" + webpack-cli: "npm:^5.1.4" webpack-dev-server: "npm:^5.0.3" ws: "npm:^8.17.1" yaml: "npm:^2.4.1" @@ -26736,7 +27251,7 @@ __metadata: languageName: node linkType: hard -"mimic-response@npm:^1.0.0": +"mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" checksum: 10/034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 @@ -26766,6 +27281,18 @@ __metadata: languageName: node linkType: hard +"mini-css-extract-plugin@npm:^2.9.1": + version: 2.9.1 + resolution: "mini-css-extract-plugin@npm:2.9.1" + dependencies: + schema-utils: "npm:^4.0.0" + tapable: "npm:^2.2.1" + peerDependencies: + webpack: ^5.0.0 + checksum: 10/a4a0c73a054254784b9d39a3a4f117691600355125242dfc46ced0912b4937050823478bdbf403b5392c21e2fb2203902b41677d67c7d668f77b985b594e94c6 + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -27149,6 +27676,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.0 + resolution: "mrmime@npm:2.0.0" + checksum: 10/8d95f714ea200c6cf3e3777cbc6168be04b05ac510090a9b41eef5ec081efeb1d1de3e535ffb9c9689fffcc42f59864fd52a500e84a677274f070adeea615c45 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -27674,6 +28208,13 @@ __metadata: languageName: node linkType: hard +"normalize-url@npm:^4.1.0": + version: 4.5.1 + resolution: "normalize-url@npm:4.5.1" + checksum: 10/20ced2845fcfaa46da74efc0aa39b7bed22f3db39e6e8b844261613082a36a2dcd468decad89fa9313b5464bebab4034f96bda7880e8fc468027fecf6a6fa254 + languageName: node + linkType: hard + "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -28216,6 +28757,13 @@ __metadata: languageName: node linkType: hard +"p-cancelable@npm:^1.0.0": + version: 1.1.0 + resolution: "p-cancelable@npm:1.1.0" + checksum: 10/2db3814fef6d9025787f30afaee4496a8857a28be3c5706432cbad76c688a6db1874308f48e364a42f5317f5e41e8e7b4f2ff5c8ff2256dbb6264bc361704ece + languageName: node + linkType: hard + "p-cancelable@npm:^2.0.0": version: 2.1.1 resolution: "p-cancelable@npm:2.1.1" @@ -28408,6 +28956,18 @@ __metadata: languageName: node linkType: hard +"package-json@npm:^6.3.0": + version: 6.5.0 + resolution: "package-json@npm:6.5.0" + dependencies: + got: "npm:^9.6.0" + registry-auth-token: "npm:^4.0.0" + registry-url: "npm:^5.0.0" + semver: "npm:^6.2.0" + checksum: 10/adb8e49f352ea0d71a4d351732c3870d57f21e6f3921d69a83dd9ef04b45cdb0a035495826fbe9fb2cb9a7e521484404b7d527c181133867b126588efa1996c6 + languageName: node + linkType: hard + "pako@npm:~0.2.0": version: 0.2.9 resolution: "pako@npm:0.2.9" @@ -29127,6 +29687,19 @@ __metadata: languageName: node linkType: hard +"postcss-import@npm:^14.1.0": + version: 14.1.0 + resolution: "postcss-import@npm:14.1.0" + dependencies: + postcss-value-parser: "npm:^4.0.0" + read-cache: "npm:^1.0.0" + resolve: "npm:^1.1.7" + peerDependencies: + postcss: ^8.0.0 + checksum: 10/434ab43145ad6beeb3cd7405596cb29920061e9d55091196e0264daf0a4e543a8cf1568c233e5a4466786749f904c03a9d51d406685055af2a14a8337d8773d5 + languageName: node + linkType: hard + "postcss-less@npm:^3.1.4": version: 3.1.4 resolution: "postcss-less@npm:3.1.4" @@ -29154,6 +29727,24 @@ __metadata: languageName: node linkType: hard +"postcss-load-config@npm:^4.0.1": + version: 4.0.2 + resolution: "postcss-load-config@npm:4.0.2" + dependencies: + lilconfig: "npm:^3.0.0" + yaml: "npm:^2.3.4" + peerDependencies: + postcss: ">=8.0.9" + ts-node: ">=9.0.0" + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + checksum: 10/e2c2ed9b7998a5b123e1ce0c124daf6504b1454c67dcc1c8fdbcc5ffb2597b7de245e3ac34f63afc928d3fd3260b1e36492ebbdb01a9ff63f16b3c8b7b925d1b + languageName: node + linkType: hard + "postcss-loader@npm:^8.1.1": version: 8.1.1 resolution: "postcss-loader@npm:8.1.1" @@ -29320,7 +29911,7 @@ __metadata: languageName: node linkType: hard -"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": +"postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" checksum: 10/e4e4486f33b3163a606a6ed94f9c196ab49a37a7a7163abfcd469e5f113210120d70b8dd5e33d64636f41ad52316a3725655421eb9a1094f1bcab1db2f555c62 @@ -29408,6 +29999,13 @@ __metadata: languageName: node linkType: hard +"prepend-http@npm:^2.0.0": + version: 2.0.0 + resolution: "prepend-http@npm:2.0.0" + checksum: 10/7694a9525405447662c1ffd352fcb41b6410c705b739b6f4e3a3e21cf5fdede8377890088e8934436b8b17ba55365a615f153960f30877bf0d0392f9e93503ea + languageName: node + linkType: hard + "preserve@npm:^0.2.0": version: 0.2.0 resolution: "preserve@npm:0.2.0" @@ -29802,6 +30400,15 @@ __metadata: languageName: node linkType: hard +"pupa@npm:^2.1.1": + version: 2.1.1 + resolution: "pupa@npm:2.1.1" + dependencies: + escape-goat: "npm:^2.0.0" + checksum: 10/49529e50372ffdb0cccf0efa0f3b3cb0a2c77805d0d9cc2725bd2a0f6bb414631e61c93a38561b26be1259550b7bb6c2cb92315aa09c8bf93f3bdcb49f2b2fb7 + languageName: node + linkType: hard + "puppeteer-core@npm:^2.1.1": version: 2.1.1 resolution: "puppeteer-core@npm:2.1.1" @@ -30061,7 +30668,7 @@ __metadata: languageName: node linkType: hard -"rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.8": +"rc@npm:1.2.8, rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" dependencies: @@ -30693,6 +31300,15 @@ __metadata: languageName: node linkType: hard +"read-cache@npm:^1.0.0": + version: 1.0.0 + resolution: "read-cache@npm:1.0.0" + dependencies: + pify: "npm:^2.3.0" + checksum: 10/83a39149d9dfa38f0c482ea0d77b34773c92fef07fe7599cdd914d255b14d0453e0229ef6379d8d27d6947f42d7581635296d0cfa7708f05a9bd8e789d398b31 + languageName: node + linkType: hard + "read-cmd-shim@npm:^4.0.0": version: 4.0.0 resolution: "read-cmd-shim@npm:4.0.0" @@ -30901,6 +31517,15 @@ __metadata: languageName: node linkType: hard +"rechoir@npm:^0.8.0": + version: 0.8.0 + resolution: "rechoir@npm:0.8.0" + dependencies: + resolve: "npm:^1.20.0" + checksum: 10/ad3caed8afdefbc33fbc30e6d22b86c35b3d51c2005546f4e79bcc03c074df804b3640ad18945e6bef9ed12caedc035655ec1082f64a5e94c849ff939dc0a788 + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -31096,6 +31721,15 @@ __metadata: languageName: node linkType: hard +"registry-auth-token@npm:^4.0.0": + version: 4.2.2 + resolution: "registry-auth-token@npm:4.2.2" + dependencies: + rc: "npm:1.2.8" + checksum: 10/00d1b1c69f09df52a0bfbaecee71f2ba094d8fd8d1abc325090655b2c6c8a69c969b31525086c10f95126c3452cd4a0c5c9a6832fb08bec5a32a4e224b790cf8 + languageName: node + linkType: hard + "registry-url@npm:^3.0.3": version: 3.1.0 resolution: "registry-url@npm:3.1.0" @@ -31105,6 +31739,15 @@ __metadata: languageName: node linkType: hard +"registry-url@npm:^5.0.0": + version: 5.1.0 + resolution: "registry-url@npm:5.1.0" + dependencies: + rc: "npm:^1.2.8" + checksum: 10/bcea86c84a0dbb66467b53187fadebfea79017cddfb4a45cf27530d7275e49082fe9f44301976eb0164c438e395684bcf3dae4819b36ff9d1640d8cc60c73df9 + languageName: node + linkType: hard + "regjsgen@npm:^0.8.0": version: 0.8.0 resolution: "regjsgen@npm:0.8.0" @@ -31648,6 +32291,15 @@ __metadata: languageName: node linkType: hard +"responselike@npm:^1.0.2": + version: 1.0.2 + resolution: "responselike@npm:1.0.2" + dependencies: + lowercase-keys: "npm:^1.0.0" + checksum: 10/2e9e70f1dcca3da621a80ce71f2f9a9cad12c047145c6ece20df22f0743f051cf7c73505e109814915f23f9e34fb0d358e22827723ee3d56b623533cab8eafcd + languageName: node + linkType: hard + "responselike@npm:^2.0.0": version: 2.0.1 resolution: "responselike@npm:2.0.1" @@ -32457,6 +33109,15 @@ __metadata: languageName: node linkType: hard +"semver-diff@npm:^3.1.1": + version: 3.1.1 + resolution: "semver-diff@npm:3.1.1" + dependencies: + semver: "npm:^6.3.0" + checksum: 10/8bbe5a5d7add2d5e51b72314a9215cd294d71f41cdc2bf6bd59ee76411f3610b576172896f1d191d0d7294cb9f2f847438d2ee158adacc0c224dca79052812fe + languageName: node + linkType: hard + "semver-greatest-satisfied-range@npm:^1.1.0": version: 1.1.0 resolution: "semver-greatest-satisfied-range@npm:1.1.0" @@ -32493,7 +33154,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -32848,6 +33509,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^2.0.2": + version: 2.0.4 + resolution: "sirv@npm:2.0.4" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10/24f42cf06895017e589c9d16fc3f1c6c07fe8b0dbafce8a8b46322cfba67b7f2498610183954cb0e9d089c8cb60002a7ee7e8bca6a91a0d7042bfbc3473c95c3 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -33471,7 +34143,7 @@ __metadata: languageName: node linkType: hard -"string-hash@npm:^1.1.1": +"string-hash@npm:^1.1.1, string-hash@npm:^1.1.3": version: 1.1.3 resolution: "string-hash@npm:1.1.3" checksum: 10/104b8667a5e0dc71bfcd29fee09cb88c6102e27bfb07c55f95535d90587d016731d52299380052e514266f4028a7a5172e0d9ac58e2f8f5001be61dc77c0754d @@ -33505,7 +34177,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -34110,6 +34782,13 @@ __metadata: languageName: node linkType: hard +"svg-parser@npm:^2.0.4": + version: 2.0.4 + resolution: "svg-parser@npm:2.0.4" + checksum: 10/ec196da6ea21481868ab26911970e35488361c39ead1c6cdd977ba16c885c21a91ddcbfd113bfb01f79a822e2a751ef85b2f7f95e2cb9245558ebce12c34af1f + languageName: node + linkType: hard + "svg-tags@npm:^1.0.0": version: 1.0.0 resolution: "svg-tags@npm:1.0.0" @@ -34536,6 +35215,13 @@ __metadata: languageName: node linkType: hard +"to-readable-stream@npm:^1.0.0": + version: 1.0.0 + resolution: "to-readable-stream@npm:1.0.0" + checksum: 10/a99e23d49777d9d03686f03cc0bbbcb4648d991648990a98bc93b55cf91a2ae830c41b5efa36802f1c00a34bba93bd33b10346772fd3f49bcf1667a99c85f354 + languageName: node + linkType: hard + "to-regex-range@npm:^2.1.0": version: 2.1.1 resolution: "to-regex-range@npm:2.1.1" @@ -34611,6 +35297,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10/5132d562cf88ff93fd710770a92f31dbe67cc19b5c6ccae2efc0da327f0954d211bbfd9456389655d726c624f284b4a23112f56d1da931ca7cfabbe1f45e778a + languageName: node + linkType: hard + "tough-cookie@npm:^4.0.0, tough-cookie@npm:^4.1.2": version: 4.1.3 resolution: "tough-cookie@npm:4.1.3" @@ -35653,6 +36346,28 @@ __metadata: languageName: node linkType: hard +"update-notifier@npm:^5.1.0": + version: 5.1.0 + resolution: "update-notifier@npm:5.1.0" + dependencies: + boxen: "npm:^5.0.0" + chalk: "npm:^4.1.0" + configstore: "npm:^5.0.1" + has-yarn: "npm:^2.1.0" + import-lazy: "npm:^2.1.0" + is-ci: "npm:^2.0.0" + is-installed-globally: "npm:^0.4.0" + is-npm: "npm:^5.0.0" + is-yarn-global: "npm:^0.3.0" + latest-version: "npm:^5.1.0" + pupa: "npm:^2.1.1" + semver: "npm:^7.3.4" + semver-diff: "npm:^3.1.1" + xdg-basedir: "npm:^4.0.0" + checksum: 10/9df39e2d4f2e59ea788c719baaacf3d2bdde09d065f00319d52c0af255990e15f98ba40c115fb6246b6b2d5468685f36955ae0679c0b7fec834892fe7db4cab2 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -35678,6 +36393,15 @@ __metadata: languageName: node linkType: hard +"url-parse-lax@npm:^3.0.0": + version: 3.0.0 + resolution: "url-parse-lax@npm:3.0.0" + dependencies: + prepend-http: "npm:^2.0.0" + checksum: 10/1040e357750451173132228036aff1fd04abbd43eac1fb3e4fca7495a078bcb8d33cb765fe71ad7e473d9c94d98fd67adca63bd2716c815a2da066198dd37217 + languageName: node + linkType: hard + "url-parse@npm:^1.5.3": version: 1.5.10 resolution: "url-parse@npm:1.5.10" @@ -36501,6 +37225,38 @@ __metadata: languageName: node linkType: hard +"webpack-cli@npm:^5.1.4": + version: 5.1.4 + resolution: "webpack-cli@npm:5.1.4" + dependencies: + "@discoveryjs/json-ext": "npm:^0.5.0" + "@webpack-cli/configtest": "npm:^2.1.1" + "@webpack-cli/info": "npm:^2.0.2" + "@webpack-cli/serve": "npm:^2.0.5" + colorette: "npm:^2.0.14" + commander: "npm:^10.0.1" + cross-spawn: "npm:^7.0.3" + envinfo: "npm:^7.7.3" + fastest-levenshtein: "npm:^1.0.12" + import-local: "npm:^3.0.2" + interpret: "npm:^3.1.1" + rechoir: "npm:^0.8.0" + webpack-merge: "npm:^5.7.3" + peerDependencies: + webpack: 5.x.x + peerDependenciesMeta: + "@webpack-cli/generators": + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + bin: + webpack-cli: bin/cli.js + checksum: 10/9ac3ae7c43b032051de2803d751bd3b44e1f226b931dcd56066a8e01b12734d49730903df9235e1eb1b67b2ee7451faf24a219c8f4a229c4f42c42e827eac44c + languageName: node + linkType: hard + "webpack-dev-middleware@npm:^6.1.1": version: 6.1.1 resolution: "webpack-dev-middleware@npm:6.1.1" @@ -36595,6 +37351,17 @@ __metadata: languageName: node linkType: hard +"webpack-merge@npm:^5.7.3": + version: 5.10.0 + resolution: "webpack-merge@npm:5.10.0" + dependencies: + clone-deep: "npm:^4.0.1" + flat: "npm:^5.0.2" + wildcard: "npm:^2.0.0" + checksum: 10/fa46ab200f17d06c7cb49fc37ad91f15769753953c9724adac1061fa305a2a223cb37c3ed25a5f501580c91f11a0800990fe3814c70a77bf1aa5b3fca45a2ac6 + languageName: node + linkType: hard + "webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3" @@ -36828,6 +37595,15 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^3.1.0": + version: 3.1.0 + resolution: "widest-line@npm:3.1.0" + dependencies: + string-width: "npm:^4.0.0" + checksum: 10/03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 + languageName: node + linkType: hard + "wif@npm:^4.0.0": version: 4.0.0 resolution: "wif@npm:4.0.0" @@ -36837,6 +37613,13 @@ __metadata: languageName: node linkType: hard +"wildcard@npm:^2.0.0": + version: 2.0.1 + resolution: "wildcard@npm:2.0.1" + checksum: 10/e0c60a12a219e4b12065d1199802d81c27b841ed6ad6d9d28240980c73ceec6f856771d575af367cbec2982d9ae7838759168b551776577f155044f5a5ba843c + languageName: node + linkType: hard + "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" @@ -37027,6 +37810,13 @@ __metadata: languageName: node linkType: hard +"xdg-basedir@npm:^4.0.0": + version: 4.0.0 + resolution: "xdg-basedir@npm:4.0.0" + checksum: 10/0073d5b59a37224ed3a5ac0dd2ec1d36f09c49f0afd769008a6e9cd3cd666bd6317bd1c7ce2eab47e1de285a286bad11a9b038196413cd753b79770361855f3c + languageName: node + linkType: hard + "xhr2@npm:0.2.1": version: 0.2.1 resolution: "xhr2@npm:0.2.1" @@ -37125,12 +37915,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.4.1": - version: 2.4.1 - resolution: "yaml@npm:2.4.1" +"yaml@npm:^2.3.4, yaml@npm:^2.4.1": + version: 2.6.0 + resolution: "yaml@npm:2.6.0" bin: yaml: bin.mjs - checksum: 10/2c54fd69ef59126758ae710f9756405a7d41abcbb61aca894250d0e81e76057c14dc9bb00a9528f72f99b8f24077f694a6f7fd09cdd6711fcec2eebfbb5df409 + checksum: 10/f4369f667c7626c216ea81b5840fe9b530cdae4cff2d84d166ec1239e54bf332dbfac4a71bf60d121f8e85e175364a4e280a520292269b6cf9d074368309adf9 languageName: node linkType: hard