From a166147abab910c4b294ea93239ca5f2b284c533 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 27 Jun 2024 10:16:34 -0600 Subject: [PATCH 01/15] feat(ramps): introduces dynamic support for rampable networks (#24041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces a new reducer for Ramps to store an array of "buyable" networks. A "buyable chain" is one that the native token has onramp support for. This BUYABLE_CHAINS_MAP list is currently hard-coded, and this PR fetches a dynamic network list from the Ramps API instead. The list of supported networks by the MetMask onramp team is dynamic and is based on provider support among other things. There are several fallback protections in place. Buyable chains will default to the current hard-coded list before loading and will default to that same list if there are any errors. There is no need for loading or error states. The ramps base API url has been added as a new environment variable, defaulted to production. example: METAMASK_RAMP_API_BASE_URL=https://on-ramp-content.metaswap.codefi.network Here's screenshot to show the issue this PR will fix. Base network is supported as a buyable network. but because the hard-coded array of networks does not include base we do not enable the buy CTAs: ![image](https://github.com/MetaMask/metamask-extension/assets/15269424/76023685-06a5-4ff7-9f59-8beda3dd9593) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24041?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** **Video Description of the changes in the PR (5 min)** https://www.loom.com/share/973960816e7e497aae51ed1cdc3cebf5 ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Nicolas Ferro Co-authored-by: Pedro Pablo Aste Kompen --- builds.yml | 2 + privacy-snapshot.json | 2 + shared/constants/network.ts | 118 ----------- test/data/mock-send-state.json | 137 +++++++++++++ test/data/mock-state.json | 137 +++++++++++++ .../errors-after-init-opt-in-ui-state.json | 1 + .../app/add-network/add-network.test.js | 1 + ui/components/app/asset-list/asset-list.js | 6 +- ui/components/app/nfts-tab/nfts-tab.js | 4 +- .../selected-account-component.test.js | 1 + .../transaction-list.component.js | 5 +- .../app/wallet-overview/coin-buttons.tsx | 3 +- .../app/wallet-overview/eth-overview.js | 7 +- .../app/wallet-overview/eth-overview.test.js | 26 +-- .../multichain/ramps-card/ramps-card.js | 2 +- ui/ducks/index.js | 2 + ui/ducks/ramps/constants.ts | 137 +++++++++++++ ui/ducks/ramps/index.ts | 1 + ui/ducks/ramps/ramps.test.ts | 187 ++++++++++++++++++ ui/ducks/ramps/ramps.ts | 78 ++++++++ ui/ducks/ramps/types.ts | 7 + ui/helpers/ramps/rampApi/rampAPI.test.ts | 23 +++ ui/helpers/ramps/rampApi/rampAPI.ts | 25 +++ .../useRamps/useRamps.test.tsx} | 23 ++- .../useRamps}/useRamps.ts | 4 +- ui/hooks/useTheme.test.ts | 1 + ui/pages/asset/components/asset-page.tsx | 4 +- ui/pages/asset/components/token-buttons.tsx | 10 +- .../confirm-page-container.component.js | 10 +- .../confirm-page-container.container.js | 3 - .../confirm-transaction-base.container.js | 4 +- .../confirm-transaction-base.test.js | 5 + .../hooks/useConfirmationAlertActions.ts | 2 +- .../hooks/useTransactionFunction.test.js | 1 + .../send/gas-display/gas-display.js | 6 +- ui/pages/home/home.component.js | 5 +- ui/pages/home/home.container.js | 2 + ui/pages/routes/routes.component.test.js | 6 +- .../swaps/prepare-swap-page/review-quote.js | 2 +- ui/selectors/selectors.js | 6 - 40 files changed, 812 insertions(+), 194 deletions(-) create mode 100644 ui/ducks/ramps/constants.ts create mode 100644 ui/ducks/ramps/index.ts create mode 100644 ui/ducks/ramps/ramps.test.ts create mode 100644 ui/ducks/ramps/ramps.ts create mode 100644 ui/ducks/ramps/types.ts create mode 100644 ui/helpers/ramps/rampApi/rampAPI.test.ts create mode 100644 ui/helpers/ramps/rampApi/rampAPI.ts rename ui/hooks/{experiences/useRamps.test.js => ramps/useRamps/useRamps.test.tsx} (85%) rename ui/hooks/{experiences => ramps/useRamps}/useRamps.ts (90%) diff --git a/builds.yml b/builds.yml index 2e449c687591..aa93f756db49 100644 --- a/builds.yml +++ b/builds.yml @@ -266,6 +266,8 @@ env: # Enables the notifications feature within the build: - NOTIFICATIONS: '' + - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io + ### # Meta variables ### diff --git a/privacy-snapshot.json b/privacy-snapshot.json index b45cf79a6e8f..fe6579bfab73 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -30,6 +30,8 @@ "phishing-detection.api.cx.metamask.io", "portfolio.metamask.io", "price.api.cx.metamask.io", + "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "proxy.api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 452f0584ffaa..7754b2a0f16f 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -66,22 +66,6 @@ export type RPCDefinition = { rpcPrefs: RPCPreferences; }; -/** - * For each chain that we support fiat onramps for, we provide a set of - * configuration options that help for initializing the connectiong to the - * onramp providers. - */ -type BuyableChainSettings = { - /** - * The native currency for the given chain - */ - nativeCurrency: CurrencySymbol | TestNetworkCurrencySymbol; - /** - * The network name or identifier - */ - network: string; -}; - /** * Throughout the extension we set the current provider by referencing its * "type", which can be any of the values in the below object. These values @@ -908,108 +892,6 @@ export const UNSUPPORTED_RPC_METHODS = new Set([ export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link'; -// The first item in transakCurrencies must be the -// default crypto currency for the network -const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum'; - -export const BUYABLE_CHAINS_MAP: { - [K in Exclude< - ChainId, - | typeof CHAIN_IDS.LOCALHOST - | typeof CHAIN_IDS.OPTIMISM_TESTNET - | typeof CHAIN_IDS.OPTIMISM_GOERLI - | typeof CHAIN_IDS.BASE_TESTNET - | typeof CHAIN_IDS.OPBNB_TESTNET - | typeof CHAIN_IDS.OPBNB - | typeof CHAIN_IDS.BSC_TESTNET - | typeof CHAIN_IDS.POLYGON_TESTNET - | typeof CHAIN_IDS.AVALANCHE_TESTNET - | typeof CHAIN_IDS.FANTOM_TESTNET - | typeof CHAIN_IDS.MOONBEAM_TESTNET - | typeof CHAIN_IDS.LINEA_GOERLI - | typeof CHAIN_IDS.LINEA_SEPOLIA - | typeof CHAIN_IDS.GOERLI - | typeof CHAIN_IDS.SEPOLIA - | typeof CHAIN_IDS.GNOSIS - | typeof CHAIN_IDS.AURORA - | typeof CHAIN_IDS.ARBITRUM_GOERLI - | typeof CHAIN_IDS.BLAST - | typeof CHAIN_IDS.FILECOIN - | typeof CHAIN_IDS.POLYGON_ZKEVM - | typeof CHAIN_IDS.SCROLL - | typeof CHAIN_IDS.SCROLL_SEPOLIA - | typeof CHAIN_IDS.WETHIO - | typeof CHAIN_IDS.CHZ - | typeof CHAIN_IDS.NUMBERS - | typeof CHAIN_IDS.SEI - >]: BuyableChainSettings; -} = { - [CHAIN_IDS.MAINNET]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME, - }, - [CHAIN_IDS.BSC]: { - nativeCurrency: CURRENCY_SYMBOLS.BNB, - network: 'bsc', - }, - [CHAIN_IDS.POLYGON]: { - nativeCurrency: CURRENCY_SYMBOLS.MATIC, - network: 'polygon', - }, - [CHAIN_IDS.AVALANCHE]: { - nativeCurrency: CURRENCY_SYMBOLS.AVALANCHE, - network: 'avaxcchain', - }, - [CHAIN_IDS.FANTOM]: { - nativeCurrency: CURRENCY_SYMBOLS.FANTOM, - network: 'fantom', - }, - [CHAIN_IDS.CELO]: { - nativeCurrency: CURRENCY_SYMBOLS.CELO, - network: 'celo', - }, - [CHAIN_IDS.OPTIMISM]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'optimism', - }, - [CHAIN_IDS.ARBITRUM]: { - nativeCurrency: CURRENCY_SYMBOLS.ARBITRUM, - network: 'arbitrum', - }, - [CHAIN_IDS.CRONOS]: { - nativeCurrency: CURRENCY_SYMBOLS.CRONOS, - network: 'cronos', - }, - [CHAIN_IDS.MOONBEAM]: { - nativeCurrency: CURRENCY_SYMBOLS.GLIMMER, - network: 'moonbeam', - }, - [CHAIN_IDS.MOONRIVER]: { - nativeCurrency: CURRENCY_SYMBOLS.MOONRIVER, - network: 'moonriver', - }, - [CHAIN_IDS.HARMONY]: { - nativeCurrency: CURRENCY_SYMBOLS.ONE, - network: 'harmony', - }, - [CHAIN_IDS.PALM]: { - nativeCurrency: CURRENCY_SYMBOLS.PALM, - network: 'palm', - }, - [CHAIN_IDS.LINEA_MAINNET]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'linea', - }, - [CHAIN_IDS.ZKSYNC_ERA]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'zksync', - }, - [CHAIN_IDS.BASE]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'base', - }, -}; - export const FEATURED_RPCS: RPCDefinition[] = [ { chainId: CHAIN_IDS.ARBITRUM, diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index c8604c96c055..5beee30824ce 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -1242,6 +1242,143 @@ ], "swapsState": {} }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "shortName": "Ethereum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "shortName": "Optimism", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "shortName": "Cronos", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "shortName": "BNB Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 100, + "chainName": "Gnosis Mainnet", + "shortName": "Gnosis", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "shortName": "Polygon", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "shortName": "Fantom", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 324, + "chainName": "zkSync Era Mainnet", + "shortName": "zkSync Era", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1101, + "chainName": "Polygon zkEVM", + "shortName": "Polygon zkEVM", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1284, + "chainName": "Moonbeam Mainnet", + "shortName": "Moonbeam", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "shortName": "Moonriver", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 8453, + "chainName": "Base Mainnet", + "shortName": "Base", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "shortName": "Arbitrum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "shortName": "Celo", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "shortName": "Avalanche C-Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 59144, + "chainName": "Linea", + "shortName": "Linea", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "shortName": "Aurora", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "shortName": "Harmony (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm Mainnet", + "shortName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": "1-tx", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 920533023a58..9e595ad8f0e0 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1943,6 +1943,143 @@ } } }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "shortName": "Ethereum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "shortName": "Optimism", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "shortName": "Cronos", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "shortName": "BNB Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 100, + "chainName": "Gnosis Mainnet", + "shortName": "Gnosis", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "shortName": "Polygon", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "shortName": "Fantom", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 324, + "chainName": "zkSync Era Mainnet", + "shortName": "zkSync Era", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1101, + "chainName": "Polygon zkEVM", + "shortName": "Polygon zkEVM", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1284, + "chainName": "Moonbeam Mainnet", + "shortName": "Moonbeam", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "shortName": "Moonriver", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 8453, + "chainName": "Base Mainnet", + "shortName": "Base", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "shortName": "Arbitrum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "shortName": "Celo", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "shortName": "Avalanche C-Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 59144, + "chainName": "Linea", + "shortName": "Linea", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "shortName": "Aurora", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "shortName": "Harmony (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm Mainnet", + "shortName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": null, 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 1dd7775ec70f..4b7972acd311 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 @@ -257,6 +257,7 @@ "encryptionKey": "string", "encryptionSalt": "string" }, + "ramps": "object", "send": "object", "swaps": "object", "unconnectedAccount": { "state": "CLOSED" } diff --git a/ui/components/app/add-network/add-network.test.js b/ui/components/app/add-network/add-network.test.js index 3a15f4d33c3e..f3084b1db065 100644 --- a/ui/components/app/add-network/add-network.test.js +++ b/ui/components/app/add-network/add-network.test.js @@ -6,6 +6,7 @@ import mockState from '../../../../test/data/mock-state.json'; import AddNetwork from './add-network'; jest.mock('../../../selectors', () => ({ + ...jest.requireActual('../../../selectors'), getNetworkConfigurations: () => ({ networkConfigurationId: { chainId: '0x539', diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 82c45a27134e..959789b8eebc 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -9,9 +9,6 @@ import { getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getShouldHideZeroBalanceTokens, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, - ///: END:ONLY_INCLUDE_IF getSelectedAccount, getPreferences, } from '../../../selectors'; @@ -48,6 +45,7 @@ import { RAMPS_CARD_VARIANT_TYPES, RampsCard, } from '../../multichain/ramps-card/ramps-card'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF const AssetList = ({ onClickAsset }) => { @@ -109,7 +107,7 @@ const AssetList = ({ onClickAsset }) => { }); const balanceIsZero = Number(totalFiatBalance) === 0; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/nfts-tab/nfts-tab.js b/ui/components/app/nfts-tab/nfts-tab.js index bc54ca4c9766..43a0bd1a9d64 100644 --- a/ui/components/app/nfts-tab/nfts-tab.js +++ b/ui/components/app/nfts-tab/nfts-tab.js @@ -17,7 +17,6 @@ import { useNftsCollections } from '../../../hooks/useNftsCollections'; import { getCurrentNetwork, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, getShouldHideZeroBalanceTokens, getSelectedAccount, ///: END:ONLY_INCLUDE_IF @@ -49,6 +48,7 @@ import { RampsCard, } from '../../multichain/ramps-card/ramps-card'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF import Spinner from '../../ui/spinner'; @@ -73,7 +73,7 @@ export default function NftsTab() { shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const showRampsCard = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF 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 a48e24a661c6..ae8a3ff000db 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -52,6 +52,7 @@ jest.mock('../../../selectors', () => { return { getAccountType: mockGetAccountType, getSelectedInternalAccount: mockGetSelectedAccount, + getCurrentChainId: jest.fn(() => '0x1'), }; }); diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index b47130c5d739..20c41ef33e4d 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -10,7 +10,6 @@ import { getCurrentChainId, getSelectedAccount, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, getShouldHideZeroBalanceTokens, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; @@ -33,6 +32,7 @@ import { RAMPS_CARD_VARIANT_TYPES, RampsCard, } from '../../multichain/ramps-card/ramps-card'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF const PAGE_INCREMENT = 10; @@ -141,8 +141,7 @@ export default function TransactionList({ shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; - const isBuyableChain = useSelector(getIsBuyableChain); - + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const showRampsCard = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 5bba897121e4..8eb24128b60e 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -55,7 +55,8 @@ import { Box, Icon, IconName } from '../../component-library'; import IconButton from '../../ui/icon-button'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; + ///: END:ONLY_INCLUDE_IF const CoinButtons = ({ diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 5eeec7a59cbf..684c6cffbf6b 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; - import { EthMethod } from '@metamask/keyring-api'; import { isEqual } from 'lodash'; import { @@ -13,15 +12,17 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getSwapsDefaultToken, getIsBridgeChain, - getIsBuyableChain, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; const EthOverview = ({ className }) => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); // FIXME: This causes re-renders, so use isEqual to avoid this const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 0d13bff7e1ef..0d079a32f104 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -12,27 +12,11 @@ import { import { renderWithProvider } from '../../../../test/jest/rendering'; import { KeyringType } from '../../../../shared/constants/keyring'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import { getIntlLocale } from '../../../ducks/locale/locale'; import EthOverview from './eth-overview'; -// Mock BUYABLE_CHAINS_MAP -jest.mock('../../../../shared/constants/network', () => ({ - ...jest.requireActual('../../../../shared/constants/network'), - BUYABLE_CHAINS_MAP: { - // MAINNET - '0x1': { - nativeCurrency: 'ETH', - network: 'ethereum', - }, - // POLYGON - '0x89': { - nativeCurrency: 'MATIC', - network: 'polygon', - }, - }, -})); - jest.mock('../../../hooks/useIsOriginalNativeTokenSymbol', () => { return { useIsOriginalNativeTokenSymbol: jest.fn(), @@ -138,6 +122,9 @@ describe('EthOverview', () => { }, ], }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const store = configureMockStore([thunk])(mockStore); @@ -181,6 +168,7 @@ describe('EthOverview', () => { it('should show the cached primary balance', async () => { const mockedStoreWithCachedBalance = { + ...mockStore, metamask: { ...mockStore.metamask, accounts: { @@ -267,6 +255,7 @@ describe('EthOverview', () => { it('should open the MMI PD Swaps URI when clicking on Swap button with a Custody account', async () => { const mockedStoreWithCustodyKeyring = { + ...mockStore, metamask: { ...mockStore.metamask, mmiConfiguration: { @@ -375,6 +364,7 @@ describe('EthOverview', () => { it('should have the Buy native token button disabled if chain id is not part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { @@ -399,6 +389,7 @@ describe('EthOverview', () => { it('should have the Buy native token enabled if chain id is part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { @@ -432,6 +423,7 @@ describe('EthOverview', () => { it('should open the Buy native token URI when clicking on Buy button for a buyable chain ID', async () => { const mockedStoreWithBuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index e1c61f2a35c1..2fa793b7e958 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -21,7 +21,7 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; import useRamps, { RampsMetaMaskEntry, -} from '../../../hooks/experiences/useRamps'; +} from '../../../hooks/ramps/useRamps/useRamps'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import { getCurrentLocale } from '../../../ducks/locale/locale'; diff --git a/ui/ducks/index.js b/ui/ducks/index.js index 145dd708cb90..f72918460655 100644 --- a/ui/ducks/index.js +++ b/ui/ducks/index.js @@ -11,6 +11,7 @@ import gasReducer from './gas/gas.duck'; import { invalidCustomNetwork, unconnectedAccount } from './alerts'; import swapsReducer from './swaps/swaps'; import historyReducer from './history/history'; +import rampsReducer from './ramps/ramps'; import confirmAlertsReducer from './confirm-alerts/confirm-alerts'; export default combineReducers({ @@ -26,6 +27,7 @@ export default combineReducers({ confirmAlerts: confirmAlertsReducer, confirmTransaction: confirmTransactionReducer, swaps: swapsReducer, + ramps: rampsReducer, gas: gasReducer, localeMessages: localeMessagesReducer, }); diff --git a/ui/ducks/ramps/constants.ts b/ui/ducks/ramps/constants.ts new file mode 100644 index 000000000000..7a451658807e --- /dev/null +++ b/ui/ducks/ramps/constants.ts @@ -0,0 +1,137 @@ +import { AggregatorNetwork } from './types'; + +export const defaultBuyableChains: AggregatorNetwork[] = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + shortName: 'Ethereum', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 10, + chainName: 'Optimism Mainnet', + shortName: 'Optimism', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 25, + chainName: 'Cronos Mainnet', + shortName: 'Cronos', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 56, + chainName: 'BNB Chain Mainnet', + shortName: 'BNB Chain', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 100, + chainName: 'Gnosis Mainnet', + shortName: 'Gnosis', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 137, + chainName: 'Polygon Mainnet', + shortName: 'Polygon', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 250, + chainName: 'Fantom Mainnet', + shortName: 'Fantom', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 324, + chainName: 'zkSync Era Mainnet', + shortName: 'zkSync Era', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1101, + chainName: 'Polygon zkEVM', + shortName: 'Polygon zkEVM', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1284, + chainName: 'Moonbeam Mainnet', + shortName: 'Moonbeam', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1285, + chainName: 'Moonriver Mainnet', + shortName: 'Moonriver', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 8453, + chainName: 'Base Mainnet', + shortName: 'Base', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42161, + chainName: 'Arbitrum Mainnet', + shortName: 'Arbitrum', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42220, + chainName: 'Celo Mainnet', + shortName: 'Celo', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 43114, + chainName: 'Avalanche C-Chain Mainnet', + shortName: 'Avalanche C-Chain', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 59144, + chainName: 'Linea', + shortName: 'Linea', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1313161554, + chainName: 'Aurora Mainnet', + shortName: 'Aurora', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 1666600000, + chainName: 'Harmony Mainnet (Shard 0)', + shortName: 'Harmony (Shard 0)', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 11297108109, + chainName: 'Palm Mainnet', + shortName: 'Palm', + nativeTokenSupported: false, + }, +]; diff --git a/ui/ducks/ramps/index.ts b/ui/ducks/ramps/index.ts new file mode 100644 index 000000000000..b6f8f2473810 --- /dev/null +++ b/ui/ducks/ramps/index.ts @@ -0,0 +1 @@ +export * from './ramps'; diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts new file mode 100644 index 000000000000..c4ca4089815a --- /dev/null +++ b/ui/ducks/ramps/ramps.test.ts @@ -0,0 +1,187 @@ +import { configureStore, Store } from '@reduxjs/toolkit'; +import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; +import { getCurrentChainId, getUseExternalServices } from '../../selectors'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import rampsReducer, { + fetchBuyableChains, + getBuyableChains, + getIsNativeTokenBuyable, +} from './ramps'; +import { defaultBuyableChains } from './constants'; + +jest.mock('../../helpers/ramps/rampApi/rampAPI'); +const mockedRampAPI = RampAPI as jest.Mocked; + +jest.mock('../../selectors', () => ({ + getCurrentChainId: jest.fn(), + getUseExternalServices: jest.fn(), + getNames: jest.fn(), +})); + +describe('rampsSlice', () => { + let store: Store; + + beforeEach(() => { + store = configureStore({ + reducer: { + ramps: rampsReducer, + }, + }); + mockedRampAPI.getNetworks.mockReset(); + }); + + it('should set the initial state to defaultBuyableChains', () => { + const { ramps: rampsState } = store.getState(); + expect(rampsState).toEqual({ + buyableChains: defaultBuyableChains, + }); + }); + + describe('setBuyableChains', () => { + it('should update the buyableChains state when setBuyableChains is dispatched', () => { + const mockBuyableChains = [{ chainId: '0x1' }]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(mockBuyableChains); + }); + it('should disregard invalid array and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: 'Invalid array', + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should disregard empty array and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: [], + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should disregard array with invalid elements and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: ['some invalid', 'element'], + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + }); + + describe('getBuyableChains', () => { + it('returns buyableChains', () => { + const state = store.getState(); + expect(getBuyableChains(state)).toBe(state.ramps.buyableChains); + }); + }); + + describe('fetchBuyableChains', () => { + beforeEach(() => { + // simulate the Basic Functionality Toggle being on + const getUseExternalServicesMock = jest.mocked(getUseExternalServices); + getUseExternalServicesMock.mockReturnValue(true); + }); + + it('should call RampAPI.getNetworks when the Basic Functionality Toggle is on', async () => { + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + expect(RampAPI.getNetworks).toHaveBeenCalledTimes(1); + }); + + it('should not call RampAPI.getNetworks when the Basic Functionality Toggle is off', async () => { + const getUseExternalServicesMock = jest.mocked(getUseExternalServices); + getUseExternalServicesMock.mockReturnValue(false); + + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + + expect(RampAPI.getNetworks).not.toHaveBeenCalled(); + }); + + it('should update the state with the data that is returned', async () => { + const mockBuyableChains = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + shortName: 'Ethereum', + }, + ]; + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(mockBuyableChains); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(mockBuyableChains); + }); + it('should set state to defaultBuyableChains when returned networks are undefined', async () => { + // @ts-expect-error forcing undefined to test the behavior + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(undefined); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should set state to defaultBuyableChains when returned networks are empty', async () => { + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue([]); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should set state to defaultBuyableChains when API request fails', async () => { + jest + .spyOn(RampAPI, 'getNetworks') + .mockRejectedValue(new Error('API error')); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + }); + + describe('getIsNativeTokenBuyable', () => { + const getCurrentChainIdMock = jest.mocked(getCurrentChainId); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return true when current chain is buyable', () => { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(true); + }); + + it('should return false when current chain is not buyable', () => { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.GOERLI); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(false); + }); + + it('should return false when current chain is not a valid hex string', () => { + getCurrentChainIdMock.mockReturnValue('0x'); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(false); + }); + + it('should return false when buyable chains is a corrupted array', () => { + const mockState = { + ramps: { + buyableChains: [null, null, null], + }, + }; + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + expect(getIsNativeTokenBuyable(mockState)).toEqual(false); + }); + }); +}); diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts new file mode 100644 index 000000000000..afff609cd4d8 --- /dev/null +++ b/ui/ducks/ramps/ramps.ts @@ -0,0 +1,78 @@ +import { createSelector } from 'reselect'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { getCurrentChainId, getUseExternalServices } from '../../selectors'; +import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; +import { hexToDecimal } from '../../../shared/modules/conversion.utils'; +import { defaultBuyableChains } from './constants'; +import { AggregatorNetwork } from './types'; + +export const fetchBuyableChains = createAsyncThunk( + 'ramps/fetchBuyableChains', + async (_, { getState }) => { + const state = getState(); + const allowExternalRequests = getUseExternalServices(state); + if (!allowExternalRequests) { + return defaultBuyableChains; + } + return await RampAPI.getNetworks(); + }, +); + +const rampsSlice = createSlice({ + name: 'ramps', + initialState: { + buyableChains: defaultBuyableChains, + }, + reducers: { + setBuyableChains: (state, action) => { + if ( + Array.isArray(action.payload) && + action.payload.length > 0 && + action.payload.every((network) => network?.chainId) + ) { + state.buyableChains = action.payload; + } else { + state.buyableChains = defaultBuyableChains; + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchBuyableChains.fulfilled, (state, action) => { + const networks = action.payload; + if (networks && networks.length > 0) { + state.buyableChains = networks; + } else { + state.buyableChains = defaultBuyableChains; + } + }) + .addCase(fetchBuyableChains.rejected, (state) => { + state.buyableChains = defaultBuyableChains; + }); + }, +}); + +const { reducer } = rampsSlice; + +// Can be typed to RootState if/when the interface is defined +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getBuyableChains = (state: any) => + state.ramps?.buyableChains ?? defaultBuyableChains; + +export const getIsNativeTokenBuyable = createSelector( + [getCurrentChainId, getBuyableChains], + (currentChainId, buyableChains) => { + try { + return buyableChains + .filter(Boolean) + .some( + (network: AggregatorNetwork) => + String(network.chainId) === hexToDecimal(currentChainId), + ); + } catch (e) { + return false; + } + }, +); + +export default reducer; diff --git a/ui/ducks/ramps/types.ts b/ui/ducks/ramps/types.ts new file mode 100644 index 000000000000..6a1571715dfe --- /dev/null +++ b/ui/ducks/ramps/types.ts @@ -0,0 +1,7 @@ +export type AggregatorNetwork = { + active: boolean; + chainId: number; + chainName: string; + nativeTokenSupported: boolean; + shortName: string; +}; diff --git a/ui/helpers/ramps/rampApi/rampAPI.test.ts b/ui/helpers/ramps/rampApi/rampAPI.test.ts new file mode 100644 index 000000000000..bf6e2297481d --- /dev/null +++ b/ui/helpers/ramps/rampApi/rampAPI.test.ts @@ -0,0 +1,23 @@ +import nock from 'nock'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; +import rampAPI from './rampAPI'; + +const mockedResponse = { + networks: defaultBuyableChains, +}; + +describe('rampAPI', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('should fetch networks', async () => { + nock('https://on-ramp-content.uat-api.cx.metamask.io') + .get('/regions/networks') + .query(true) + .reply(200, mockedResponse); + + const result = await rampAPI.getNetworks(); + expect(result).toStrictEqual(mockedResponse.networks); + }); +}); diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts new file mode 100644 index 000000000000..a1da6da8ef0c --- /dev/null +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -0,0 +1,25 @@ +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; +import { AggregatorNetwork } from '../../../ducks/ramps/types'; + +const fetchWithTimeout = getFetchWithTimeout(); + +const isProdEnv = process.env.NODE_ENV === 'production'; +const PROD_RAMP_API_BASE_URL = 'https://on-ramp-content.api.cx.metamask.io'; +const UAT_RAMP_API_BASE_URL = 'https://on-ramp-content.uat-api.cx.metamask.io'; + +const rampApiBaseUrl = + process.env.METAMASK_RAMP_API_CONTENT_BASE_URL || + (isProdEnv ? PROD_RAMP_API_BASE_URL : UAT_RAMP_API_BASE_URL); + +const RampAPI = { + async getNetworks(): Promise { + const url = new URL('/regions/networks', rampApiBaseUrl); + url.searchParams.set('context', 'extension'); + const response = await fetchWithTimeout(url.toString()); + + const { networks } = await response.json(); + return networks; + }, +}; + +export default RampAPI; diff --git a/ui/hooks/experiences/useRamps.test.js b/ui/hooks/ramps/useRamps/useRamps.test.tsx similarity index 85% rename from ui/hooks/experiences/useRamps.test.js rename to ui/hooks/ramps/useRamps/useRamps.test.tsx index dcb1dc2eb43c..9c53eb9da247 100644 --- a/ui/hooks/experiences/useRamps.test.js +++ b/ui/hooks/ramps/useRamps/useRamps.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { FC } from 'react'; import { Provider } from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; -import configureStore from '../../store/store'; +import configureStore from '../../../store/store'; import useRamps, { RampsMetaMaskEntry } from './useRamps'; const mockedMetametricsId = '0xtestMetaMetricsId'; @@ -15,17 +15,18 @@ let mockStoreState = { }, }; -const wrapper = ({ children }) => ( +const wrapper: FC = ({ children }) => ( {children} ); describe('useRamps', () => { - beforeEach(() => { - global.platform = { openTab: jest.fn() }; - }); - - afterEach(() => { - jest.clearAllMocks(); + // mock the openTab function to test if it is called with the correct URL when opening the Pdapp + beforeAll(() => { + Object.defineProperty(global, 'platform', { + value: { + openTab: jest.fn(), + }, + }); }); it('should default the metamask entry param when opening the buy crypto URL', () => { @@ -81,9 +82,11 @@ describe('useRamps', () => { }); }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore it.each(['0x1', '0x38', '0xa'])( 'should open the buy crypto URL with the currently connected chain ID', - (mockChainId) => { + (mockChainId: string) => { mockStoreState = { ...mockStoreState, metamask: { diff --git a/ui/hooks/experiences/useRamps.ts b/ui/hooks/ramps/useRamps/useRamps.ts similarity index 90% rename from ui/hooks/experiences/useRamps.ts rename to ui/hooks/ramps/useRamps/useRamps.ts index 76bcdfe47879..7219d5fe4193 100644 --- a/ui/hooks/experiences/useRamps.ts +++ b/ui/hooks/ramps/useRamps/useRamps.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import type { Hex } from '@metamask/utils'; -import { ChainId } from '../../../shared/constants/network'; -import { getCurrentChainId, getMetaMetricsId } from '../../selectors'; +import { ChainId } from '../../../../shared/constants/network'; +import { getCurrentChainId, getMetaMetricsId } from '../../../selectors'; type IUseRamps = { openBuyCryptoInPdapp: VoidFunction; diff --git a/ui/hooks/useTheme.test.ts b/ui/hooks/useTheme.test.ts index 8b0f2edd66ac..5b44d2c918eb 100644 --- a/ui/hooks/useTheme.test.ts +++ b/ui/hooks/useTheme.test.ts @@ -3,6 +3,7 @@ import { renderHookWithProvider } from '../../test/lib/render-helpers'; import { useTheme } from './useTheme'; jest.mock('../selectors', () => ({ + ...jest.requireActual('../selectors'), getTheme: jest.fn(), })); diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index d83f6cfe24f8..38ea46e475f3 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -7,7 +7,6 @@ import { isEqual } from 'lodash'; import { getCurrentCurrency, getIsBridgeChain, - getIsBuyableChain, getIsSwapsChain, getSelectedInternalAccount, getSwapsDefaultToken, @@ -42,6 +41,7 @@ import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getConversionRate } from '../../../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import CoinButtons from '../../../components/app/wallet-overview/coin-buttons'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; @@ -102,7 +102,7 @@ const AssetPage = ({ const conversionRate = useSelector(getConversionRate); const allMarketData = useSelector(getTokensMarketData); const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const account = useSelector(getSelectedInternalAccount, isEqual); const isSwapsChain = useSelector(getIsSwapsChain); diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index f0e39f046e24..6cd78fab7693 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -13,7 +13,7 @@ import { startNewDraftTransaction } from '../../../ducks/send'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -28,11 +28,9 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getIsBridgeChain, getCurrentKeyring, - getIsBuyableChain, getMetaMetricsId, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; - import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; import { showModal } from '../../../store/actions'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -42,7 +40,6 @@ import { MetaMetricsSwapsEventSource, } from '../../../../shared/constants/metametrics'; import { AssetType } from '../../../../shared/constants/transaction'; - import { Display, IconColor, @@ -50,6 +47,9 @@ import { } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; import { Box, Icon, IconName } from '../../../components/component-library'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { Asset } from './asset-page'; const TokenButtons = ({ @@ -71,7 +71,7 @@ const TokenButtons = ({ const isSwapsChain = useSelector(getIsSwapsChain); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const metaMetricsId = useSelector(getMetaMetricsId); const { openBuyCryptoInPdapp } = useRamps(); ///: END:ONLY_INCLUDE_IF diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js index d177bc6b8df2..d4a92172aed6 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js @@ -43,12 +43,11 @@ import { getAccountName, getAddressBookEntry, getInternalAccounts, - getIsBuyableChain, getMetadataContractName, getNetworkIdentifier, getSwapsDefaultToken, } from '../../../../selectors'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { @@ -58,6 +57,7 @@ import { ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -118,7 +118,7 @@ const ConfirmPageContainer = (props) => { const [collectionBalance, setCollectionBalance] = useState('0'); const [isShowingTxInsightWarnings, setIsShowingTxInsightWarnings] = useState(false); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const contact = useSelector((state) => getAddressBookEntry(state, toAddress)); const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); @@ -131,10 +131,6 @@ const ConfirmPageContainer = (props) => { getMetadataContractName(state, toAddress), ); - // TODO: Move useRamps hook to the confirm-transaction-base parent component. - // TODO: openBuyCryptoInPdapp should be passed to this component as a custom prop. - // We try to keep this component for layout purpose only, we need to move this hook to the confirm-transaction-base parent - // component once it is converted to a functional component const { openBuyCryptoInPdapp } = useRamps(); const isSetApproveForAll = diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js index be3ba7793eaf..db07ad4117e7 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { getAddressBookEntry, - getIsBuyableChain, getNetworkIdentifier, getSwapsDefaultToken, getMetadataContractName, @@ -12,7 +11,6 @@ import ConfirmPageContainer from './confirm-page-container.component'; function mapStateToProps(state, ownProps) { const to = ownProps.toAddress; - const isBuyableChain = getIsBuyableChain(state); const contact = getAddressBookEntry(state, to); const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); @@ -23,7 +21,6 @@ function mapStateToProps(state, ownProps) { const toMetadataName = getMetadataContractName(state, to); return { - isBuyableChain, contact, toName, toMetadataName, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index abe09d6f63dc..dc225367090c 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -47,7 +47,6 @@ import { getPreferences, doesAddressRequireLedgerHidConnection, getTokenList, - getIsBuyableChain, getEnsResolutionByAddress, getUnapprovedTransaction, getFullTxData, @@ -109,6 +108,7 @@ import { showCustodyConfirmLink } from '../../../store/institutional/institution ///: END:ONLY_INCLUDE_IF import { calcGasTotal } from '../../../../shared/lib/transactions-controller-utils'; import { subtractHexes } from '../../../../shared/modules/conversion.utils'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import ConfirmTransactionBase from './confirm-transaction-base.component'; let customNonceValue = ''; @@ -164,7 +164,7 @@ const mapStateToProps = (state, ownProps) => { const isGasEstimatesLoading = getIsGasEstimatesLoading(state); const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); - const isBuyableChain = getIsBuyableChain(state); + const isBuyableChain = getIsNativeTokenBuyable(state); const { confirmTransaction, metamask } = state; const conversionRate = getConversionRate(state); const { addressBook, nextNonce } = metamask; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index 918c24ed6681..bfa23af0af75 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -27,6 +27,7 @@ import { BlockaidReason, BlockaidResultType, } from '../../../../shared/constants/security-provider'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import ConfirmTransactionBase from './confirm-transaction-base.container'; @@ -203,6 +204,9 @@ const baseStore = { appState: { sendInputCurrencySwitched: false, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const mockedStoreWithConfirmTxParams = ( @@ -497,6 +501,7 @@ describe('Confirm Transaction Base', () => { it('handleMMISubmit calls sendTransaction correctly and then showCustodianDeepLink', async () => { const state = { + ...baseStore, appState: { ...baseStore.appState, gasLoadingAnimationIsShowing: false, diff --git a/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts b/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts index fae80bc53316..b02ef8f809a6 100644 --- a/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts +++ b/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { AlertActionKey } from '../../../components/app/confirm/info/row/constants'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { useTransactionModalContext } from '../../../contexts/transaction-modal'; const useConfirmationAlertActions = () => { diff --git a/ui/pages/confirmations/hooks/useTransactionFunction.test.js b/ui/pages/confirmations/hooks/useTransactionFunction.test.js index cc4e69863892..22984ba71a37 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunction.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunction.test.js @@ -22,6 +22,7 @@ useGasEstimates.mockImplementation(() => FEE_MARKET_ESTIMATE_RETURN_VALUE); jest.mock('../../../selectors', () => ({ checkNetworkAndAccountSupports1559: () => true, + getCurrentChainId: jest.fn().mockReturnValue('0x1'), })); const wrapper = ({ children }) => ( diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index e07c5473bdb9..5fbad8445cd6 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -23,7 +23,6 @@ import TransactionDetail from '../../components/transaction-detail'; import ActionableMessage from '../../../../components/ui/actionable-message'; import { getPreferences, - getIsBuyableChain, transactionFeeSelector, getIsTestnet, getUseCurrencyRateCheck, @@ -46,7 +45,8 @@ import { MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); @@ -58,7 +58,7 @@ export default function GasDisplay({ gasError }) { const providerConfig = useSelector(getProviderConfig); const isTestnet = useSelector(getIsTestnet); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const { showFiatInTestnets, useNativeCurrencyAsPrimaryCurrency } = diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 3a3dfc7ed753..cb177340501d 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -25,8 +25,8 @@ import Popover from '../../components/ui/popover'; import ConnectedSites from '../connected-sites'; import ConnectedAccounts from '../connected-accounts'; import { isMv3ButOffscreenDocIsMissing } from '../../../shared/modules/mv3.utils'; - import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; + import { FontWeight, Display, @@ -220,6 +220,7 @@ export default class Home extends PureComponent { custodianDeepLink: PropTypes.object, accountType: PropTypes.string, ///: END:ONLY_INCLUDE_IF + fetchBuyableChains: PropTypes.func.isRequired, }; state = { @@ -362,6 +363,8 @@ export default class Home extends PureComponent { setWaitForConfirmDeepLinkDialog(false); }); ///: END:ONLY_INCLUDE_IF + + this.props.fetchBuyableChains(); } static getDerivedStateFromProps(props) { diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 7b1334f80e0e..ea9c7171448a 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -85,6 +85,7 @@ import { } from '../../ducks/app/app'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; import { getSwapsFeatureIsLive } from '../../ducks/swaps/swaps'; +import { fetchBuyableChains } from '../../ducks/ramps'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { getIsBrowserDeprecated } from '../../helpers/utils/util'; import { @@ -319,6 +320,7 @@ const mapDispatchToProps = (dispatch) => { ///: END:ONLY_INCLUDE_IF setBasicFunctionalityModalOpen: () => dispatch(openBasicFunctionalityModal()), + fetchBuyableChains: () => dispatch(fetchBuyableChains()), }; }; diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index 8c7d127c88e1..3024ea436e35 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -1,7 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { act } from '@testing-library/react'; - +import thunk from 'redux-thunk'; import { SEND_STAGES } from '../../ducks/send'; import { CONFIRMATION_V_NEXT_ROUTE, @@ -14,6 +14,8 @@ import mockState from '../../../test/data/mock-state.json'; import { useIsOriginalNativeTokenSymbol } from '../../hooks/useIsOriginalNativeTokenSymbol'; import Routes from '.'; +const middlewares = [thunk]; + const mockShowNetworkDropdown = jest.fn(); const mockHideNetworkDropdown = jest.fn(); @@ -74,7 +76,7 @@ jest.mock('../../helpers/utils/feature-flags', () => ({ })); const render = async (route, state) => { - const store = configureMockStore()({ + const store = configureMockStore(middlewares)({ ...mockSendState, ...state, }); diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 5fb2badd8bcb..0dc3f89009cb 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -141,7 +141,7 @@ import { import { GAS_FEES_LEARN_MORE_URL } from '../../../../shared/lib/ui-utils'; import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index f613015efb7c..cd049873dfa4 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -13,7 +13,6 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import { addHexPrefix, getEnvironmentType } from '../../app/scripts/lib/util'; import { TEST_CHAINS, - BUYABLE_CHAINS_MAP, MAINNET_DISPLAY_NAME, BSC_DISPLAY_NAME, POLYGON_DISPLAY_NAME, @@ -1307,11 +1306,6 @@ export function getIsBridgeChain(state) { const chainId = getCurrentChainId(state); return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId); } - -export function getIsBuyableChain(state) { - const chainId = getCurrentChainId(state); - return Object.keys(BUYABLE_CHAINS_MAP).includes(chainId); -} export function getNativeCurrencyImage(state) { const chainId = getCurrentChainId(state); return CHAIN_ID_TOKEN_IMAGE_MAP[chainId]; From 66bf2f19632feeb44c6a981c18db497ae2754596 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:21:48 +0200 Subject: [PATCH 02/15] feat: Add domain binding SIWE redesign alert row (#25281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Adds Alert for Domain Binding SIWE - Minor out-of-scope cleanup: - update useBlockaidAlert to use currentConfirmationSelector instead of useCurrentConfirmation c21feb210598157a5ca1b5c667ebb5324dfb1ab0 - force return boolean for isSIWESignatureRequest f779176f54f3529d2c1ee1d94e53d3be2c089cb1 Currently, the existing logic displays the same friction modal for multiple alerts with only one danger alert. There will be a separate PR to update the Alert System to follow the designs mentioned in the Issue ticket ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/24683 ## **Manual testing steps** 1. comment out code in useCurrentConfirmation: ``` // // comment if condition below to enable re-design for SIWE signatures ``` 2. yarn start 3. open dapp and test SIWE Bad Domain ## **Screenshots/Recordings** ### **Before** ### **After** (There is another PR that is hiding the "i" icon beside the Alert for SIWE) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 6 + test/data/confirmations/personal_sign.ts | 27 ++-- .../__snapshots__/siwe-sign.test.tsx.snap | 24 ++++ .../personal-sign/siwe-sign/siwe-sign.tsx | 2 +- .../useDomainMismatchAlerts.test.ts | 135 ++++++++++++++++++ .../signatures/useDomainMismatchAlerts.ts | 45 ++++++ .../hooks/useConfirmationAlerts.ts | 12 +- .../hooks/useCurrentConfirmation.ts | 4 + ui/pages/confirmations/types/confirm.ts | 18 +-- ui/pages/confirmations/utils/confirm.ts | 2 +- 10 files changed, 246 insertions(+), 29 deletions(-) create mode 100644 ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts create mode 100644 ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 53961ff86b35..d59f9d572f6c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -412,6 +412,9 @@ "alertMessagePendingTransactions": { "message": "This transaction won’t go through until a previous transaction is complete. Learn how to cancel or speed up a transaction." }, + "alertMessageSignInDomainMismatch": { + "message": "The site making the request is not the site you’re signing into. This could be an attempt to steal your login credentials." + }, "alertMessageSigningOrSubmitting": { "message": "This transaction will only go through once your previous transaction is complete." }, @@ -445,6 +448,9 @@ "alertReasonPendingTransactions": { "message": "Pending transaction" }, + "alertReasonSignIn": { + "message": "Suspicious sign-in request" + }, "alertSettingsUnconnectedAccount": { "message": "Browsing a website with an unconnected account selected" }, diff --git a/test/data/confirmations/personal_sign.ts b/test/data/confirmations/personal_sign.ts index bac5b3f9838a..6ac82dfe57bb 100644 --- a/test/data/confirmations/personal_sign.ts +++ b/test/data/confirmations/personal_sign.ts @@ -35,15 +35,20 @@ export const signatureRequestSIWE = { siwe: { isSIWEMessage: true, parsedMessage: { - domain: 'metamask.github.io', address: '0x935e73edb9ff52e23bac7f7e049a1ecd06d05477', + chainId: 1, + domain: 'metamask.github.io', + expirationTime: null, + issuedAt: '2021-09-30T16:25:24.000Z', + nonce: '32891757', + notBefore: '2022-03-17T12:45:13.610Z', + requestId: 'some_id', + scheme: null, statement: 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', uri: 'https://metamask.github.io', version: '1', - chainId: 1, - nonce: '32891757', - issuedAt: '2021-09-30T16:25:24.000Z', + resources: null, }, }, }, @@ -67,17 +72,19 @@ export const SignatureRequestSIWEWithResources = { siwe: { isSIWEMessage: true, parsedMessage: { - domain: 'metamask.github.io', address: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', - statement: - 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', - uri: 'https://metamask.github.io', - version: '1', chainId: 1, - nonce: '32891757', + domain: 'metamask.github.io', + expirationTime: null, issuedAt: '2021-09-30T16:25:24.000Z', + nonce: '32891757', notBefore: '2022-03-17T12:45:13.610Z', requestId: 'some_id', + scheme: null, + statement: + 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', + uri: 'https://metamask.github.io', + version: '1', resources: [ 'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu', 'https://example.com/my-web2-claim.json', diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap index 1e62756cf223..d6b27a2629ad 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/__snapshots__/siwe-sign.test.tsx.snap @@ -241,6 +241,30 @@ exports[`SIWESignInfo renders correctly for SIWE signature request 1`] = `

+
+
+

+ Request ID +

+
+
+

+ some_id +

+
+
`; diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx index 94389b253450..8c00c16f9c02 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx @@ -44,7 +44,7 @@ const SIWESignInfo: React.FC = () => { return ( <> - + diff --git a/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts new file mode 100644 index 000000000000..bfb63ee8496f --- /dev/null +++ b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.test.ts @@ -0,0 +1,135 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { Severity } from '../../../../../helpers/constants/design-system'; +import { renderHookWithProvider } from '../../../../../../test/lib/render-helpers'; +import mockState from '../../../../../../test/data/mock-state.json'; +import useDomainMismatchAlert from './useDomainMismatchAlerts'; + +const MOCK_ORIGIN = 'https://example-dapp.example'; +const MOCK_SUSPICIOUS_DOMAIN = 'http://suspicious.example'; +const MOCK_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + +const mockSiwe = { + isSIWEMessage: true, + parsedMessage: { + domain: MOCK_SUSPICIOUS_DOMAIN, + address: MOCK_ADDRESS, + statement: + 'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos', + uri: 'http://localhost:8080', + version: '1', + nonce: 'STMt6KQMwwdOXE306', + chainId: 1, + issuedAt: '2023-03-18T21:40:40.823Z', + resources: [ + 'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu', + 'https://example.com/my-web2-claim.json', + ], + }, +}; + +const mockCurrentConfirmation = { + id: '1', + status: 'unapproved', + time: new Date().getTime(), + type: ApprovalType.PersonalSign, + msgParams: { + from: MOCK_ADDRESS, + data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', + origin: MOCK_ORIGIN, + siwe: mockSiwe, + }, +}; + +const mockExpectedState = { + ...mockState, + metamask: { + ...mockState.metamask, + unapprovedPersonalMsgs: { + '1': { ...mockCurrentConfirmation }, + }, + pendingApprovals: { + '1': { + ...mockCurrentConfirmation, + // origin: MOCK_ORIGIN, + requestData: {}, + requestState: null, + expectsResult: false, + }, + }, + preferences: { redesignedConfirmationsEnabled: true }, + }, + confirm: { currentConfirmation: mockCurrentConfirmation }, +}; + +describe('useDomainMismatchAlert', () => { + beforeAll(() => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'true'; + }); + + afterAll(() => { + process.env.ENABLE_CONFIRMATION_REDESIGN = 'false'; + }); + + describe('returns an empty array', () => { + it('when there is no current confirmation', () => { + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + mockState, + ); + expect(result.current).toEqual([]); + }); + + it('when the current confirmation is not a SIWE request', () => { + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + { + ...mockExpectedState, + confirm: { + currentConfirmation: { + ...mockCurrentConfirmation, + msgParams: { + ...mockCurrentConfirmation.msgParams, + siwe: { + isSIWEMessage: false, + parsedMessage: mockSiwe.parsedMessage, + }, + }, + }, + }, + }, + ); + expect(result.current).toEqual([]); + }); + + it('when the SIWE domain matches origin', () => { + const originalDomain = mockSiwe.parsedMessage.domain; + mockSiwe.parsedMessage.domain = MOCK_ORIGIN; + + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + mockExpectedState, + ); + expect(result.current).toEqual([]); + + mockSiwe.parsedMessage.domain = originalDomain; + }); + }); + + it('returns an alert when the SIWE domain does not match the origin', () => { + const alertResponseExpected = { + field: 'requestFrom', + key: 'requestFrom', + message: + 'The site making the request is not the site you’re signing into. This could be an attempt to steal your login credentials.', + reason: 'Suspicious sign-in request', + severity: Severity.Danger, + }; + const { result } = renderHookWithProvider( + () => useDomainMismatchAlert(), + mockExpectedState, + ); + + expect(result.current).toHaveLength(1); + expect(result.current[0]).toStrictEqual(alertResponseExpected); + }); +}); diff --git a/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts new file mode 100644 index 000000000000..f75aa657ec2c --- /dev/null +++ b/ui/pages/confirmations/hooks/alerts/signatures/useDomainMismatchAlerts.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { + isValidSIWEOrigin, + WrappedSIWERequest, +} from '@metamask/controller-utils'; + +import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; +import { Severity } from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { currentConfirmationSelector } from '../../../../../selectors'; + +import { SignatureRequestType } from '../../../types/confirm'; +import { isSIWESignatureRequest } from '../../../utils'; + +export default function useDomainMismatchAlerts(): Alert[] { + const t = useI18nContext(); + + const currentConfirmation = useSelector( + currentConfirmationSelector, + ) as SignatureRequestType; + const { msgParams } = currentConfirmation || {}; + + const isSIWE = isSIWESignatureRequest(currentConfirmation); + const isInvalidSIWEDomain = + isSIWE && !isValidSIWEOrigin(msgParams as WrappedSIWERequest); + + const alerts = useMemo(() => { + if (!isInvalidSIWEDomain) { + return []; + } + + return [ + { + field: 'requestFrom', + key: 'requestFrom', + message: t('alertMessageSignInDomainMismatch'), + reason: t('alertReasonSignIn'), + severity: Severity.Danger, + }, + ] as Alert[]; + }, [isInvalidSIWEDomain, t]); + + return alerts; +} diff --git a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts index 5bbfe2a721e2..9b93b285b871 100644 --- a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts +++ b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { Alert } from '../../../ducks/confirm-alerts/confirm-alerts'; import useBlockaidAlerts from './alerts/useBlockaidAlerts'; +import useDomainMismatchAlerts from './alerts/signatures/useDomainMismatchAlerts'; import { useInsufficientBalanceAlerts } from './alerts/transactions/useInsufficientBalanceAlerts'; import { useGasEstimateFailedAlerts } from './alerts/transactions/useGasEstimateFailedAlerts'; import { usePendingTransactionAlerts } from './alerts/transactions/usePendingTransactionAlerts'; @@ -10,6 +11,12 @@ import { useGasTooLowAlerts } from './alerts/transactions/useGasTooLowAlerts'; import { useNoGasPriceAlerts } from './alerts/transactions/useNoGasPriceAlerts'; import { useNetworkBusyAlerts } from './alerts/transactions/useNetworkBusyAlerts'; +function useSignatureAlerts(): Alert[] { + const domainMismatchAlerts = useDomainMismatchAlerts(); + + return useMemo(() => [...domainMismatchAlerts], [domainMismatchAlerts]); +} + function useTransactionAlerts(): Alert[] { const gasEstimateFailedAlerts = useGasEstimateFailedAlerts(); const gasFeeLowAlerts = useGasFeeLowAlerts(); @@ -46,10 +53,11 @@ function useTransactionAlerts(): Alert[] { export default function useConfirmationAlerts(): Alert[] { const blockaidAlerts = useBlockaidAlerts(); + const signatureAlerts = useSignatureAlerts(); const transactionAlerts = useTransactionAlerts(); return useMemo( - () => [...blockaidAlerts, ...transactionAlerts], - [blockaidAlerts, transactionAlerts], + () => [...blockaidAlerts, ...signatureAlerts, ...transactionAlerts], + [blockaidAlerts, signatureAlerts, transactionAlerts], ); } diff --git a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts index 77356e848cde..880eedca7399 100644 --- a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts +++ b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts @@ -63,6 +63,10 @@ const useCurrentConfirmation = () => { if ( !redesignedConfirmationsEnabled || (!isCorrectTransactionType && !isCorrectApprovalType) || + /** + * @todo remove isSIWE check when we want to enable SIWE in redesigned confirmations + * @see {@link https://github.com/MetaMask/metamask-extension/issues/24617} + */ isSIWE ) { return { currentConfirmation: undefined }; diff --git a/ui/pages/confirmations/types/confirm.ts b/ui/pages/confirmations/types/confirm.ts index 2050aae32c05..36a1b6a31397 100644 --- a/ui/pages/confirmations/types/confirm.ts +++ b/ui/pages/confirmations/types/confirm.ts @@ -1,4 +1,6 @@ import { ApprovalControllerState } from '@metamask/approval-controller'; +import { SIWEMessage } from '@metamask/controller-utils'; + import { TransactionMeta, TransactionType, @@ -27,21 +29,7 @@ export type SignatureRequestType = { data: string | TypedSignDataV1Type; version?: string; signatureMethod?: string; - siwe?: { - isSIWEMessage: boolean; - parsedMessage: null | { - domain: string; - address: string; - statement: string; - uri: string; - version: string; - chainId: number; - nonce: string; - issuedAt: string; - requestId?: string; - resources?: string[]; - }; - }; + siwe?: SIWEMessage; }; type: TransactionType; custodyId?: string; diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 3325f8dd81d1..73fa4450d3f5 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -46,7 +46,7 @@ export const parseSanitizeTypedDataMessage = (dataToParse: string) => { }; export const isSIWESignatureRequest = (request: SignatureRequestType) => - request.msgParams?.siwe?.isSIWEMessage; + Boolean(request?.msgParams?.siwe?.isSIWEMessage); export const isPermitSignatureRequest = (request: SignatureRequestType) => { if ( From e01083d0273238fca4006cdb159c7034afbeffe2 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:29:29 +0200 Subject: [PATCH 03/15] refactor: Confirm Alert Modal - no logic changes (#25371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** No logic changes; Only refactor / cleanup - split Confirm footer button w/ condition `hasDangerAlerts` - deprecate `inlineAlert` translation; replace with `alert` dupe - rename variables - confirmAlertModalAcknowledgeBlockaid → confirmAlertModalAcknowledgeSingle - confirmAlertModalAcknowledge → confirmAlertModalAcknowledgeMultiple [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25371?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/de/messages.json | 5 +-- app/_locales/el/messages.json | 5 +-- app/_locales/en/messages.json | 7 ++-- app/_locales/es/messages.json | 5 +-- app/_locales/fr/messages.json | 5 +-- app/_locales/hi/messages.json | 5 +-- app/_locales/id/messages.json | 5 +-- app/_locales/ja/messages.json | 5 +-- app/_locales/ko/messages.json | 5 +-- app/_locales/pt/messages.json | 5 +-- app/_locales/ru/messages.json | 5 +-- app/_locales/tl/messages.json | 5 +-- app/_locales/tr/messages.json | 5 +-- app/_locales/vi/messages.json | 5 +-- app/_locales/zh_CN/messages.json | 5 +-- .../confirm-alert-modal.tsx | 4 +-- .../__snapshots__/inline-alert.test.tsx.snap | 6 ++-- .../inline-alert/inline-alert.tsx | 2 +- .../components/confirm/footer/footer.tsx | 34 +++++++++++++------ .../simulation-error-message.js | 1 + 20 files changed, 46 insertions(+), 78 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 2500ff05f254..0160c032ed88 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Bestätigen" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Ich habe die Benachrichtigungen zur Kenntnis genommen und möchte trotzdem fortfahren" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Ihre erste Transaktion wurde vom Netzwerk bestätigt. Klicken Sie auf „OK“, um zurückzukehren." }, - "inlineAlert": { - "message": "Warnung" - }, "inputLogicEmptyState": { "message": "Geben Sie nur eine Nummer ein, die Sie den Drittanbieter jetzt oder in Zukunft ausgeben lassen möchten. Sie können die Ausgabenobergrenze später jederzeit ändern." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index bff686407193..a53b48a7be40 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Επιβεβαίωση" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Έχω ενημερωθεί για τις ειδοποιήσεις και εξακολουθώ να θέλω να συνεχίσω" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Η αρχική σας συναλλαγή επιβεβαιώθηκε από το δίκτυο. Κάντε κλικ στο OK για να επιστρέψετε." }, - "inlineAlert": { - "message": "Ειδοποίηση" - }, "inputLogicEmptyState": { "message": "Πληκτρολογήστε μόνο έναν αριθμό που σας βολεύει να ξοδέψει ο τρίτος τώρα ή στο μέλλον. Μπορείτε πάντα να αυξήσετε το ανώτατο όριο δαπανών αργότερα." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index d59f9d572f6c..6590fea16133 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -951,10 +951,10 @@ "confirm": { "message": "Confirm" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "I have acknowledged the alerts and still want to proceed" }, - "confirmAlertModalAcknowledgeBlockaid": { + "confirmAlertModalAcknowledgeSingle": { "message": "I have acknowledged the alert and still want to proceed" }, "confirmAlertModalDetails": { @@ -2301,9 +2301,6 @@ "initialTransactionConfirmed": { "message": "Your initial transaction was confirmed by the network. Click OK to go back." }, - "inlineAlert": { - "message": "Alert" - }, "inputLogicEmptyState": { "message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 6a70bf40e68f..244c70313634 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "Confirmar" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Soy consciente de las alertas y aun así deseo continuar" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "La red confirmó la transacción inicial. Haga clic en Aceptar para volver." }, - "inlineAlert": { - "message": "Alerta" - }, "inputLogicEmptyState": { "message": "Ingrese solo una cantidad que esté dispuesto a gastar en el tercero ahora o en el futuro. Siempre puede aumentar el límite de gastos más adelante." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 0cd50d4788f8..1b2a54f3c4c1 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Confirmer" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "J’ai pris connaissance des alertes, mais je souhaite quand même continuer" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Votre transaction initiale a été confirmée par le réseau. Cliquez sur OK pour retourner à l’écran précédent." }, - "inlineAlert": { - "message": "Alerte" - }, "inputLogicEmptyState": { "message": "N'entrez qu'une somme que vous pouvez accepter que le tiers dépense aujourd'hui ou à l'avenir. Vous pourrez toujours augmenter le plafond de dépenses ultérieurement." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 5f83ad80bfdf..dc6318004958 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "कन्फर्म करें" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "मैंने एलर्ट को स्वीकार कर लिया है और इसके बावजूद आगे बढ़ना चाहता/चाहती हूं" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "नेटवर्क द्वारा आपके प्रारंभिक ट्रांसेक्शन को कन्फर्म किया गया था। वापस जाने के लिए ठीक पर क्लिक करें।" }, - "inlineAlert": { - "message": "एलर्ट" - }, "inputLogicEmptyState": { "message": "केवल वही संख्या डालें जो आप अभी या भविष्य में थर्ड पार्टी खर्च के साथ सहज महसूस करते हैं। आप बाद में कभी भी खर्च करने की लिमिट बढ़ा सकते हैं।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 45073d2af42b..0faef246fd39 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Konfirmasikan" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Saya telah mengetahui peringatannya dan tetap ingin melanjutkan" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Transaksi awal Anda dikonfirmasikan oleh jaringan. Klik Oke untuk kembali." }, - "inlineAlert": { - "message": "Peringatan" - }, "inputLogicEmptyState": { "message": "Masukkan angka yang menurut Anda dapat digunakan pihak ketiga sekarang atau di masa mendatang. Anda selalu dapat meningkatkan batas penggunaan nantinya." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ad69880ffd8e..4308665d7bcb 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "確認" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "アラートを確認したうえで続行します" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "最初のトランザクションはネットワークによって承認されました。戻るには「OK」をクリックします。" }, - "inlineAlert": { - "message": "アラート" - }, "inputLogicEmptyState": { "message": "現在または今後サードパーティが使用しても構わない額のみを入力してください。使用上限は後でいつでも増額できます。" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 9121a9b84281..464c3bf9e250 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "컨펌" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "경고를 인지했으며, 계속 진행합니다" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "최초 트랜잭션을 네트워크에서 컨펌했습니다. 돌아가려면 컨펌을 클릭하세요." }, - "inlineAlert": { - "message": "경고" - }, "inputLogicEmptyState": { "message": "타사에서 현재나 추후 지출하기에 무리가 없는 금액만 입력하세요. 지출 한도는 나중에 언제든지 상향할 수 있습니다." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e83e1b6d4eea..f201f475a4ec 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Confirmar" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Confirmo que recebi os alertas e ainda quero prosseguir" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Sua transação inicial foi confirmada pela rede. Clique em OK para voltar." }, - "inlineAlert": { - "message": "Alerta" - }, "inputLogicEmptyState": { "message": "Somente insira um número com o qual esteja confortável de o terceiro gastar agora ou no futuro. Você pode aumentar o limite de gastos a qualquer momento." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index cb41df257fe1..3c419a274034 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Подтвердить" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Я подтвердил(-а) получение оповещений и все еще хочу продолжить" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "Ваша первоначальная транзакция подтверждена сетью. Нажмите ОК, чтобы вернуться." }, - "inlineAlert": { - "message": "Оповещение" - }, "inputLogicEmptyState": { "message": "Введите только ту сумму, которую третья сторона, по вашему мнению, может тратить сейчас или в будущем. Вы всегда можете увеличить лимит расходов позже." }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index cbd81acfcbc6..b6d454c012af 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "Kumpirmahin" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Kinikilala ko ang mga alerto at nais ko pa rin magpatuloy" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "Nakumpirma na ng network ang iyong inisyal na transaksyon. I-click ang OK para bumalik." }, - "inlineAlert": { - "message": "Alerto" - }, "inputLogicEmptyState": { "message": "Maglagay lamang ng numero na komportable ka sa paggastos ng third party ngayon o sa hinaharap. Maaari mong palaging taasan ang limitasyon sa paggastos sa ibang pagkakataon." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 13887164bfd1..cd6508892738 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -838,7 +838,7 @@ "confirm": { "message": "Onayla" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Uyarıları kabul ediyorum ve yine de ilerlemek istiyorum" }, "confirmAlertModalDetails": { @@ -2122,9 +2122,6 @@ "initialTransactionConfirmed": { "message": "İlk işleminiz ağ tarafından onaylanmıştır. Geri gitmek için Tamam düğmesine tıklayın." }, - "inlineAlert": { - "message": "Uyarı" - }, "inputLogicEmptyState": { "message": "Sadece şu anda ya da gelecekte üçüncü taraf harcaması konusunda rahat olduğunuz bir sayı girin. Harcama üst limitini daha sonra dilediğiniz zaman artırabilirsiniz." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 3afd6bf2403e..ffc867cf6e76 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "Xác nhận" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "Tôi đã hiểu rõ các cảnh báo và vẫn muốn tiếp tục" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "Mạng đã xác nhận giao dịch ban đầu của bạn. Nhấp OK để quay lại." }, - "inlineAlert": { - "message": "Cảnh báo" - }, "inputLogicEmptyState": { "message": "Chỉ nhập số mà bạn cảm thấy thoải mái đối với hạn mức chi tiêu ở hiện tại hoặc trong tương lai của bên thứ ba. Bạn luôn có thể tăng hạn mức chi tiêu sau này." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 6cdb6aaf1e3f..14c8e355be1f 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -835,7 +835,7 @@ "confirm": { "message": "确认" }, - "confirmAlertModalAcknowledge": { + "confirmAlertModalAcknowledgeMultiple": { "message": "我已知晓提醒并仍想继续" }, "confirmAlertModalDetails": { @@ -2119,9 +2119,6 @@ "initialTransactionConfirmed": { "message": "您的初始交易已被网络确认。请点击“确定”返回。" }, - "inlineAlert": { - "message": "提醒" - }, "inputLogicEmptyState": { "message": "仅需输入一个您觉得比较恰当的现在或将来第三方支出的数字。以后您可以随时提高支出上限。" }, diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx index c9647cedf633..0c4165182b18 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx @@ -180,8 +180,8 @@ export function ConfirmAlertModal({ onCheckboxClick={handleConfirmCheckbox} label={ selectedAlert?.provider === SecurityProvider.Blockaid - ? t('confirmAlertModalAcknowledgeBlockaid') - : t('confirmAlertModalAcknowledge') + ? t('confirmAlertModalAcknowledgeSingle') + : t('confirmAlertModalAcknowledgeMultiple') } /> } diff --git a/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap b/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap index ef56b906038f..d869e2f978fa 100644 --- a/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap +++ b/ui/components/app/alert-system/inline-alert/__snapshots__/inline-alert.test.tsx.snap @@ -16,7 +16,7 @@ exports[`Inline Alert renders alert with danger severity 1`] = `

- [inlineAlert] + [alert]

- [inlineAlert] + [alert]

- [inlineAlert] + [alert]

- {t('inlineAlert')} + {t('alert')} diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index 6d48f42ae420..a3d3e1bbc71d 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -65,17 +65,29 @@ const ConfirmButton = ({ onSubmit={onSubmit} /> )} - + {hasDangerAlerts ? ( + + ) : ( + + )} ); }; diff --git a/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js b/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js index a80bd4b6a159..0edd91ad42e0 100644 --- a/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js +++ b/ui/pages/confirmations/components/simulation-error-message/simulation-error-message.js @@ -29,6 +29,7 @@ export default function SimulationErrorMessage({ ], }, }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return userAcknowledgedGasMissing === true ? ( From 1d33d6f39a499aa6c82c4f97fc288875a55bec30 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:48:25 +0200 Subject: [PATCH 04/15] fix: flaky test `Swap-Send ETH to non-contract address with data that matches swap data signature submits a transaction successfully` (#25545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The problem is that we are looking for a pending transaction and then a confirmed transaction. If the transaction is confirmed very fast, the pending transaction is never found, thus making the test fail. This is a bad pattern that introduces flakiness, as we are trying to find an element by its transient state, which is variable. What we really want to assert is that the transaction is in the end confirmed, so we shouldn't try to look for the pending transaction (something that disappears fast) in the first place. Error: ``` TimeoutError: Waiting for element to be located By(css selector, .transaction-status-label--pending) Wait timed out after 10081ms ``` [ci error](https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/89418/workflows/924ea8b8-1698-4dca-ae6e-b724264fe4a2/jobs/3304211/parallel-runs/17?filterBy=ALL) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25545?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/25546 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** See the screenshot in ci, how the expected transaction is indeed there (confirmed). However, on the error we couldn't locate the element when it was pending (possibly due to being really fast at occasions) ![image](https://github.com/MetaMask/metamask-extension/assets/54408225/6dd42cb2-d71a-4587-b663-49a424493e72) ![Screenshot from 2024-06-27 09-51-55](https://github.com/MetaMask/metamask-extension/assets/54408225/2eab4f8e-bba1-4c23-ab7a-cb141a5a30e9) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/tests/swap-send/swap-send-eth.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/e2e/tests/swap-send/swap-send-eth.spec.ts b/test/e2e/tests/swap-send/swap-send-eth.spec.ts index 244693d513a2..e4be22ae1fa3 100644 --- a/test/e2e/tests/swap-send/swap-send-eth.spec.ts +++ b/test/e2e/tests/swap-send/swap-send-eth.spec.ts @@ -74,12 +74,6 @@ describe('Swap-Send ETH', function () { // TODO assert swap api request payload await swapSendPage.submitSwap(); - await swapSendPage.verifyHistoryEntry( - 'Send ETH as TST', - 'Pending', - '-1 ETH', - '', - ); await swapSendPage.verifyHistoryEntry( 'Send ETH as TST', 'Confirmed', From 574f81906c29e3caf251d6412d88705e7f319d00 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:04:22 -0400 Subject: [PATCH 05/15] chore: update `chokidar` to `v3.6.0` (#25571) Some old versions are old so we might as well update! --- lavamoat/build-system/policy.json | 1561 ++++++++++++----------------- package.json | 3 +- yarn.lock | 353 +------ 3 files changed, 633 insertions(+), 1284 deletions(-) diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 02078b5d39fc..a01cf9c55765 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -2124,8 +2124,8 @@ }, "chokidar>anymatch": { "packages": { - "chokidar>anymatch>picomatch": true, - "chokidar>normalize-path": true + "chokidar>anymatch>normalize-path": true, + "chokidar>anymatch>picomatch": true } }, "chokidar>anymatch>picomatch": { @@ -3945,8 +3945,8 @@ }, "gulp-sourcemaps>@gulp-sourcemaps/identity-map": { "packages": { - "chokidar>normalize-path": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>acorn": true, + "gulp-sourcemaps>@gulp-sourcemaps/identity-map>normalize-path": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>postcss": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>source-map": true, "gulp-sourcemaps>@gulp-sourcemaps/identity-map>through2": true @@ -3997,13 +3997,8 @@ }, "gulp-sourcemaps>@gulp-sourcemaps/map-sources": { "packages": { - "gulp-sourcemaps>@gulp-sourcemaps/map-sources>normalize-path": true, - "gulp-sourcemaps>@gulp-sourcemaps/map-sources>through2": true - } - }, - "gulp-sourcemaps>@gulp-sourcemaps/map-sources>normalize-path": { - "packages": { - "vinyl>remove-trailing-separator": true + "gulp-sourcemaps>@gulp-sourcemaps/map-sources>through2": true, + "gulp-watch>anymatch>normalize-path": true } }, "gulp-sourcemaps>@gulp-sourcemaps/map-sources>through2": { @@ -4254,10 +4249,10 @@ "setTimeout": true }, "packages": { + "chokidar": true, "eslint>glob-parent": true, "gulp-watch>ansi-colors": true, "gulp-watch>anymatch": true, - "gulp-watch>chokidar": true, "gulp-watch>fancy-log": true, "gulp-watch>path-is-absolute": true, "gulp-watch>readable-stream": true, @@ -4314,7 +4309,7 @@ "packages": { "gulp-watch>anymatch>micromatch>braces>expand-range": true, "gulp-watch>anymatch>micromatch>braces>preserve": true, - "gulp-watch>chokidar>braces>repeat-element": true + "gulp-watch>anymatch>micromatch>braces>repeat-element": true } }, "gulp-watch>anymatch>micromatch>braces>expand-range": { @@ -4327,7 +4322,7 @@ "gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>is-number": true, "gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>isobject": true, "gulp-watch>anymatch>micromatch>braces>expand-range>fill-range>randomatic": true, - "gulp-watch>chokidar>braces>repeat-element": true, + "gulp-watch>anymatch>micromatch>braces>repeat-element": true, "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true } }, @@ -4455,8 +4450,6 @@ }, "packages": { "chokidar>normalize-path": true, - "del>is-glob": true, - "eslint>glob-parent": true, "eslint>is-glob": true, "gulp-watch>chokidar>anymatch": true, "gulp-watch>chokidar>async-each": true, @@ -4470,750 +4463,234 @@ "pumpify>inherits": true } }, - "gulp-watch>chokidar>anymatch": { + "gulp-watch>chokidar>fsevents": { "builtin": { - "path.sep": true + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "console.assert": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true }, "packages": { - "gulp-watch>chokidar>anymatch>micromatch": true, - "gulp-watch>chokidar>anymatch>normalize-path": true + "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, - "gulp-watch>chokidar>anymatch>micromatch": { + "gulp-watch>chokidar>fsevents>node-pre-gyp": { "builtin": { - "path.basename": true, - "path.sep": true, - "util.inspect": true + "events.EventEmitter": true, + "fs.existsSync": true, + "fs.readFileSync": true, + "fs.renameSync": true, + "path.dirname": true, + "path.existsSync": true, + "path.join": true, + "path.resolve": true, + "url.parse": true, + "url.resolve": true, + "util.inherits": true }, "globals": { - "process.platform": true + "__dirname": true, + "console.log": true, + "process.arch": true, + "process.cwd": true, + "process.env": true, + "process.platform": true, + "process.version.substr": true, + "process.versions": true }, "packages": { - "@babel/register>clone-deep>kind-of": true, - "gulp-watch>chokidar>anymatch>micromatch>define-property": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob": true, - "gulp-watch>chokidar>braces": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp-zip>plugin-error>arr-diff": true, - "gulp-zip>plugin-error>extend-shallow": true, - "gulp>gulp-cli>liftoff>fined>object.pick": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>nanomatch": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>define-property": { - "packages": { - "gulp>gulp-cli>isobject": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>define-property": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>extend-shallow": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>define-property": { - "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets": { - "globals": { - "__filename": true + "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": { + "builtin": { + "child_process.spawnSync": true, + "fs.readdirSync": true, + "os.platform": true }, - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>debug": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>extend-shallow": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>gulp-cli>matchdep>micromatch>extglob>expand-brackets>posix-character-classes": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true + "globals": { + "process.env": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>debug": { + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": { "builtin": { - "fs.SyncWriteStream": true, - "net.Socket": true, - "tty.WriteStream": true, - "tty.isatty": true, - "util": true + "path": true, + "stream.Stream": true, + "url": true }, "globals": { - "chrome": true, "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true + "process.argv": true, + "process.env.DEBUG_NOPT": true, + "process.env.NOPT_DEBUG": true, + "process.platform": true }, "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>debug>ms": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": true, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": { - "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true + "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>nopt>abbrev": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": { + "builtin": { + "child_process.exec": true, + "path": true + }, + "globals": { + "process.env.COMPUTERNAME": true, + "process.env.ComSpec": true, + "process.env.EDITOR": true, + "process.env.HOSTNAME": true, + "process.env.PATH": true, + "process.env.PROMPT": true, + "process.env.PS1": true, + "process.env.Path": true, + "process.env.SHELL": true, + "process.env.USER": true, + "process.env.USERDOMAIN": true, + "process.env.USERNAME": true, + "process.env.VISUAL": true, + "process.env.path": true, + "process.nextTick": true, + "process.platform": true + }, "packages": { - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true + "@storybook/core>@storybook/core-server>x-default-browser>default-browser-id>untildify>os-homedir": true, + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true + "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": { + "globals": { + "process.env.SystemRoot": true, + "process.env.TEMP": true, + "process.env.TMP": true, + "process.env.TMPDIR": true, + "process.env.windir": true, + "process.platform": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>expand-brackets>extend-shallow": { + "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": { + "builtin": { + "assert": true, + "fs": true, + "path.join": true + }, + "globals": { + "process.platform": true, + "setTimeout": true + }, "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "nyc>glob": true } }, - "gulp-watch>chokidar>anymatch>micromatch>extglob>extend-shallow": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": { + "globals": { + "console": true, + "process": true } }, - "gulp-watch>chokidar>anymatch>normalize-path": { + "gulp-watch>fancy-log": { + "globals": { + "console": true, + "process.argv.indexOf": true, + "process.stderr.write": true, + "process.stdout.write": true + }, "packages": { - "vinyl>remove-trailing-separator": true + "fancy-log>ansi-gray": true, + "fancy-log>color-support": true, + "fancy-log>time-stamp": true } }, - "gulp-watch>chokidar>async-each": { + "gulp-watch>path-is-absolute": { "globals": { - "define": true - } - }, - "gulp-watch>chokidar>braces": { - "packages": { - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>extend-shallow": true, - "gulp-watch>chokidar>braces>fill-range": true, - "gulp-watch>chokidar>braces>repeat-element": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>snapdragon-node": true, - "gulp-watch>chokidar>braces>split-string": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>gulp-cli>isobject": true, - "gulp>undertaker>arr-flatten": true + "process.platform": true } }, - "gulp-watch>chokidar>braces>extend-shallow": { + "gulp-watch>readable-stream": { + "builtin": { + "events.EventEmitter": true, + "stream": true, + "util": true + }, + "globals": { + "process.browser": true, + "process.env.READABLE_STREAM": true, + "process.stderr": true, + "process.stdout": true, + "process.version.slice": true, + "setImmediate": true + }, "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp-watch>readable-stream>isarray": true, + "gulp-watch>readable-stream>safe-buffer": true, + "gulp-watch>readable-stream>string_decoder": true, + "pumpify>inherits": true, + "readable-stream-2>core-util-is": true, + "readable-stream-2>process-nextick-args": true, + "readable-stream>util-deprecate": true } }, - "gulp-watch>chokidar>braces>fill-range": { + "gulp-watch>readable-stream>safe-buffer": { "builtin": { - "util.inspect": true - }, - "packages": { - "gulp-watch>chokidar>braces>fill-range>extend-shallow": true, - "gulp-watch>chokidar>braces>fill-range>is-number": true, - "gulp-watch>chokidar>braces>fill-range>to-regex-range": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + "buffer": true } }, - "gulp-watch>chokidar>braces>fill-range>extend-shallow": { + "gulp-watch>readable-stream>string_decoder": { "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp-watch>readable-stream>safe-buffer": true } }, - "gulp-watch>chokidar>braces>fill-range>is-number": { + "gulp-watch>vinyl-file": { + "builtin": { + "path.resolve": true + }, + "globals": { + "process.cwd": true + }, "packages": { - "gulp-watch>chokidar>braces>fill-range>is-number>kind-of": true + "del>graceful-fs": true, + "gh-pages>globby>pinkie-promise": true, + "gulp-watch>vinyl-file>pify": true, + "gulp-watch>vinyl-file>strip-bom": true, + "gulp-watch>vinyl-file>strip-bom-stream": true, + "gulp-watch>vinyl-file>vinyl": true } }, - "gulp-watch>chokidar>braces>fill-range>is-number>kind-of": { + "gulp-watch>vinyl-file>strip-bom": { + "globals": { + "Buffer.isBuffer": true + }, "packages": { - "browserify>insert-module-globals>is-buffer": true + "gulp>vinyl-fs>remove-bom-buffer>is-utf8": true } }, - "gulp-watch>chokidar>braces>fill-range>to-regex-range": { + "gulp-watch>vinyl-file>strip-bom-stream": { "packages": { - "gulp-watch>chokidar>braces>fill-range>is-number": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + "gulp-watch>vinyl-file>strip-bom": true, + "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": true } }, - "gulp-watch>chokidar>braces>snapdragon": { + "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": { "builtin": { - "fs.readFileSync": true, - "path.dirname": true, - "util.inspect": true + "util.inherits": true }, "globals": { - "__filename": true + "Buffer.concat": true, + "setImmediate": true }, "packages": { - "gulp-watch>chokidar>braces>snapdragon>base": true, - "gulp-watch>chokidar>braces>snapdragon>debug": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true, - "gulp-watch>chokidar>braces>snapdragon>extend-shallow": true, - "gulp-watch>chokidar>braces>snapdragon>map-cache": true, - "gulp-watch>chokidar>braces>snapdragon>source-map": true, - "gulp-watch>chokidar>braces>snapdragon>use": true, - "resolve-url-loader>rework>css>source-map-resolve": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon-node>define-property": true, - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node>define-property": { - "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon-node>snapdragon-util>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base": { - "builtin": { - "util.inherits": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base": true, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils": true, - "gulp-watch>chokidar>braces>snapdragon>base>component-emitter": true, - "gulp-watch>chokidar>braces>snapdragon>base>define-property": true, - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep": true, - "gulp-watch>chokidar>braces>snapdragon>base>pascalcase": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>union-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value": true, - "gulp-watch>chokidar>braces>snapdragon>base>component-emitter": true, - "gulp>gulp-cli>array-sort>get-value": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>map-visit": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>object-visit": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>map-visit": { - "builtin": { - "util.inspect": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>object-visit": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit>object-visit": { - "packages": { - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values": true, - "gulp>gulp-cli>array-sort>get-value": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>has-value>has-values>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value": { - "packages": { - "@babel/register>clone-deep>is-plain-object": true, - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value>extend-shallow": true, - "gulp-watch>chokidar>braces>split-string": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value>extend-shallow": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>union-value": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>set-value": true, - "gulp-zip>plugin-error>arr-union": true, - "gulp>gulp-cli>array-sort>get-value": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>has-values": true, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>isobject": true, - "gulp>gulp-cli>array-sort>get-value": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>isobject": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value>has-value>isobject>isarray": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils": { - "builtin": { - "util": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true, - "gulp-zip>plugin-error>arr-union": true, - "gulp>gulp-cli>isobject": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend": { - "builtin": { - "util.inherits": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy>copy-descriptor": true, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy>kind-of": true, - "gulp-watch>chokidar>braces>snapdragon>define-property": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>class-utils>static-extend>object-copy>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>define-property": { - "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep>is-extendable": true, - "gulp>undertaker>object.reduce>for-own>for-in": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep>is-extendable": { - "packages": { - "@babel/register>clone-deep>is-plain-object": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>debug": { - "builtin": { - "fs.SyncWriteStream": true, - "net.Socket": true, - "tty.WriteStream": true, - "tty.isatty": true, - "util": true - }, - "globals": { - "chrome": true, - "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true - }, - "packages": { - "gulp-watch>chokidar>braces>snapdragon>debug>ms": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor": true, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-accessor-descriptor": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor": { - "packages": { - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>extend-shallow": { - "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true - } - }, - "gulp-watch>chokidar>braces>snapdragon>use": { - "packages": { - "@babel/register>clone-deep>kind-of": true - } - }, - "gulp-watch>chokidar>braces>split-string": { - "packages": { - "gulp-zip>plugin-error>extend-shallow": true - } - }, - "gulp-watch>chokidar>braces>to-regex": { - "packages": { - "gulp-watch>chokidar>braces>to-regex>define-property": true, - "gulp-watch>chokidar>braces>to-regex>safe-regex": true, - "gulp-zip>plugin-error>extend-shallow": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp-watch>chokidar>braces>to-regex>define-property": { - "packages": { - "gulp>gulp-cli>isobject": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true - } - }, - "gulp-watch>chokidar>braces>to-regex>safe-regex": { - "packages": { - "gulp-watch>chokidar>braces>to-regex>safe-regex>ret": true - } - }, - "gulp-watch>chokidar>fsevents": { - "builtin": { - "events.EventEmitter": true, - "fs.stat": true, - "path.join": true, - "util.inherits": true - }, - "globals": { - "__dirname": true, - "console.assert": true, - "process.nextTick": true, - "process.platform": true, - "setImmediate": true - }, - "packages": { - "gulp-watch>chokidar>fsevents>node-pre-gyp": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp": { - "builtin": { - "events.EventEmitter": true, - "fs.existsSync": true, - "fs.readFileSync": true, - "fs.renameSync": true, - "path.dirname": true, - "path.existsSync": true, - "path.join": true, - "path.resolve": true, - "url.parse": true, - "url.resolve": true, - "util.inherits": true - }, - "globals": { - "__dirname": true, - "console.log": true, - "process.arch": true, - "process.cwd": true, - "process.env": true, - "process.platform": true, - "process.version.substr": true, - "process.versions": true - }, - "packages": { - "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>detect-libc": { - "builtin": { - "child_process.spawnSync": true, - "fs.readdirSync": true, - "os.platform": true - }, - "globals": { - "process.env": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt": { - "builtin": { - "path": true, - "stream.Stream": true, - "url": true - }, - "globals": { - "console": true, - "process.argv": true, - "process.env.DEBUG_NOPT": true, - "process.env.NOPT_DEBUG": true, - "process.platform": true - }, - "packages": { - "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>nopt>abbrev": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv": { - "builtin": { - "child_process.exec": true, - "path": true - }, - "globals": { - "process.env.COMPUTERNAME": true, - "process.env.ComSpec": true, - "process.env.EDITOR": true, - "process.env.HOSTNAME": true, - "process.env.PATH": true, - "process.env.PROMPT": true, - "process.env.PS1": true, - "process.env.Path": true, - "process.env.SHELL": true, - "process.env.USER": true, - "process.env.USERDOMAIN": true, - "process.env.USERNAME": true, - "process.env.VISUAL": true, - "process.env.path": true, - "process.nextTick": true, - "process.platform": true - }, - "packages": { - "@storybook/core>@storybook/core-server>x-default-browser>default-browser-id>untildify>os-homedir": true, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>nopt>osenv>os-tmpdir": { - "globals": { - "process.env.SystemRoot": true, - "process.env.TEMP": true, - "process.env.TMP": true, - "process.env.TMPDIR": true, - "process.env.windir": true, - "process.platform": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>rimraf": { - "builtin": { - "assert": true, - "fs": true, - "path.join": true - }, - "globals": { - "process.platform": true, - "setTimeout": true - }, - "packages": { - "nyc>glob": true - } - }, - "gulp-watch>chokidar>fsevents>node-pre-gyp>semver": { - "globals": { - "console": true, - "process": true - } - }, - "gulp-watch>chokidar>is-binary-path": { - "builtin": { - "path.extname": true - }, - "packages": { - "gulp-watch>chokidar>is-binary-path>binary-extensions": true - } - }, - "gulp-watch>chokidar>readdirp": { - "builtin": { - "path.join": true, - "path.relative": true, - "util.inherits": true - }, - "globals": { - "setImmediate": true - }, - "packages": { - "del>graceful-fs": true, - "gulp-watch>chokidar>anymatch>micromatch": true, - "gulp-watch>readable-stream": true - } - }, - "gulp-watch>chokidar>upath": { - "builtin": { - "path": true - } - }, - "gulp-watch>fancy-log": { - "globals": { - "console": true, - "process.argv.indexOf": true, - "process.stderr.write": true, - "process.stdout.write": true - }, - "packages": { - "fancy-log>ansi-gray": true, - "fancy-log>color-support": true, - "fancy-log>time-stamp": true - } - }, - "gulp-watch>path-is-absolute": { - "globals": { - "process.platform": true - } - }, - "gulp-watch>readable-stream": { - "builtin": { - "events.EventEmitter": true, - "stream": true, - "util": true - }, - "globals": { - "process.browser": true, - "process.env.READABLE_STREAM": true, - "process.stderr": true, - "process.stdout": true, - "process.version.slice": true, - "setImmediate": true - }, - "packages": { - "gulp-watch>readable-stream>isarray": true, - "gulp-watch>readable-stream>safe-buffer": true, - "gulp-watch>readable-stream>string_decoder": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true - } - }, - "gulp-watch>readable-stream>safe-buffer": { - "builtin": { - "buffer": true - } - }, - "gulp-watch>readable-stream>string_decoder": { - "packages": { - "gulp-watch>readable-stream>safe-buffer": true - } - }, - "gulp-watch>vinyl-file": { - "builtin": { - "path.resolve": true - }, - "globals": { - "process.cwd": true - }, - "packages": { - "del>graceful-fs": true, - "gh-pages>globby>pinkie-promise": true, - "gulp-watch>vinyl-file>pify": true, - "gulp-watch>vinyl-file>strip-bom": true, - "gulp-watch>vinyl-file>strip-bom-stream": true, - "gulp-watch>vinyl-file>vinyl": true - } - }, - "gulp-watch>vinyl-file>strip-bom": { - "globals": { - "Buffer.isBuffer": true - }, - "packages": { - "gulp>vinyl-fs>remove-bom-buffer>is-utf8": true - } - }, - "gulp-watch>vinyl-file>strip-bom-stream": { - "packages": { - "gulp-watch>vinyl-file>strip-bom": true, - "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": true - } - }, - "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream": { - "builtin": { - "util.inherits": true - }, - "globals": { - "Buffer.concat": true, - "setImmediate": true - }, - "packages": { - "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream": true + "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream": true } }, "gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream": { @@ -5368,9 +4845,9 @@ }, "gulp>glob-watcher": { "packages": { + "chokidar": true, "gulp>glob-watcher>anymatch": true, "gulp>glob-watcher>async-done": true, - "gulp>glob-watcher>chokidar": true, "gulp>glob-watcher>is-negated-glob": true, "gulp>glob-watcher>just-debounce": true, "gulp>undertaker>object.defaults": true @@ -5381,390 +4858,585 @@ "path.sep": true }, "packages": { - "gulp>glob-watcher>anymatch>micromatch": true, - "gulp>glob-watcher>anymatch>normalize-path": true + "gulp-watch>anymatch>normalize-path": true, + "gulp>glob-watcher>anymatch>micromatch": true + } + }, + "gulp>glob-watcher>anymatch>micromatch": { + "builtin": { + "path.basename": true, + "path.sep": true, + "util.inspect": true + }, + "globals": { + "process.platform": true + }, + "packages": { + "@babel/register>clone-deep>kind-of": true, + "gulp-zip>plugin-error>arr-diff": true, + "gulp-zip>plugin-error>extend-shallow": true, + "gulp>glob-watcher>anymatch>micromatch>braces": true, + "gulp>glob-watcher>anymatch>micromatch>define-property": true, + "gulp>glob-watcher>anymatch>micromatch>extglob": true, + "gulp>gulp-cli>liftoff>fined>object.pick": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, + "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, + "gulp>gulp-cli>matchdep>micromatch>nanomatch": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces": { + "packages": { + "gulp-watch>anymatch>micromatch>braces>repeat-element": true, + "gulp>glob-watcher>anymatch>micromatch>braces>extend-shallow": true, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node": true, + "gulp>gulp-cli>matchdep>micromatch>braces>split-string": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true, + "gulp>undertaker>arr-flatten": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range": { + "builtin": { + "util.inspect": true + }, + "packages": { + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>extend-shallow": true, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number": true, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>to-regex-range": true, + "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>to-regex-range": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>braces>fill-range>is-number": true, + "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>define-property": { + "packages": { + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, + "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": { + "globals": { + "__filename": true + }, + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>extglob>expand-brackets>posix-character-classes": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": { + "builtin": { + "fs.SyncWriteStream": true, + "net.Socket": true, + "tty.WriteStream": true, + "tty.isatty": true, + "util": true + }, + "globals": { + "chrome": true, + "console": true, + "document": true, + "localStorage": true, + "navigator": true, + "process": true + }, + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug>ms": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": true, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { + "packages": { + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + } + }, + "gulp>glob-watcher>async-done": { + "globals": { + "process.nextTick": true + }, + "packages": { + "@metamask/object-multiplex>once": true, + "duplexify>end-of-stream": true, + "gulp>glob-watcher>async-done>stream-exhaust": true, + "readable-stream-2>process-nextick-args": true + } + }, + "gulp>glob-watcher>async-done>stream-exhaust": { + "builtin": { + "stream.Writable": true, + "util.inherits": true + }, + "globals": { + "setImmediate": true + } + }, + "gulp>glob-watcher>chokidar": { + "packages": { + "gulp>glob-watcher>chokidar>fsevents": true + } + }, + "gulp>glob-watcher>chokidar>fsevents": { + "builtin": { + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "console.assert": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true + }, + "packages": { + "gulp-watch>chokidar>fsevents>node-pre-gyp": true + } + }, + "gulp>glob-watcher>just-debounce": { + "globals": { + "clearTimeout": true, + "setTimeout": true + } + }, + "gulp>gulp-cli>liftoff>fined>object.pick": { + "packages": { + "gulp>gulp-cli>isobject": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node": { + "packages": { + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>define-property": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util>kind-of": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>snapdragon-node>snapdragon-util>kind-of": { + "packages": { + "browserify>insert-module-globals>is-buffer": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>braces>split-string": { + "packages": { + "gulp-zip>plugin-error>extend-shallow": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": { + "packages": { + "@babel/register>clone-deep>kind-of": true, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": { + "packages": { + "@babel/register>clone-deep>kind-of": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": { + "packages": { + "@babel/register>clone-deep>kind-of": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>fragment-cache": { + "packages": { + "gulp>gulp-cli>liftoff>fined>parse-filepath>map-cache": true } }, - "gulp>glob-watcher>anymatch>micromatch": { + "gulp>gulp-cli>matchdep>micromatch>nanomatch": { "builtin": { "path.basename": true, "path.sep": true, "util.inspect": true }, - "globals": { - "process.platform": true - }, "packages": { "@babel/register>clone-deep>kind-of": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, "gulp-zip>plugin-error>arr-diff": true, "gulp-zip>plugin-error>extend-shallow": true, - "gulp>glob-watcher>anymatch>micromatch>define-property": true, - "gulp>glob-watcher>anymatch>micromatch>extglob": true, - "gulp>glob-watcher>chokidar>braces": true, "gulp>gulp-cli>liftoff>fined>object.pick": true, + "gulp>gulp-cli>matchdep>micromatch>array-unique": true, "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>nanomatch": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true + "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>regex-not": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex": true, + "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": true } }, - "gulp>glob-watcher>anymatch>micromatch>define-property": { + "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": { "packages": { "gulp>gulp-cli>isobject": true, "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob": { - "packages": { - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true - } - }, - "gulp>glob-watcher>anymatch>micromatch>extglob>define-property": { + "gulp>gulp-cli>matchdep>micromatch>regex-not": { "packages": { - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true + "gulp-zip>plugin-error>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon": { + "builtin": { + "fs.readFileSync": true, + "path.dirname": true, + "util.inspect": true + }, "globals": { "__filename": true }, "packages": { - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": true, - "gulp>gulp-cli>matchdep>micromatch>extglob>expand-brackets>posix-character-classes": true, - "gulp>gulp-cli>matchdep>micromatch>regex-not": true + "gulp>gulp-cli>liftoff>fined>parse-filepath>map-cache": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>debug": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>extend-shallow": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>source-map": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>use": true, + "resolve-url-loader>rework>css>source-map-resolve": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base": { "builtin": { - "fs.SyncWriteStream": true, - "net.Socket": true, - "tty.WriteStream": true, - "tty.isatty": true, - "util": true + "util.inherits": true }, - "globals": { - "chrome": true, - "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true + "packages": { + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>component-emitter": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>pascalcase": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base": { + "packages": { + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>union-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>component-emitter": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>map-visit": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": true + } + }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>map-visit": { + "builtin": { + "util.inspect": true }, "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>debug>ms": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": true + "gulp>gulp-cli>isobject": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": true, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>kind-of": true + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>kind-of": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-accessor-descriptor>kind-of": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number": { "packages": { - "browserify>insert-module-globals>is-buffer": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>is-number>kind-of": { "packages": { - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true + "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>has-value>has-values>kind-of": { "packages": { "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value": { "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "@babel/register>clone-deep>is-plain-object": true, + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, + "gulp>gulp-cli>matchdep>micromatch>braces>split-string": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value>extend-shallow": true } }, - "gulp>glob-watcher>anymatch>micromatch>extglob>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value>extend-shallow": { "packages": { "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true } }, - "gulp>glob-watcher>anymatch>normalize-path": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path": { "packages": { - "vinyl>remove-trailing-separator": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path>kind-of": true } }, - "gulp>glob-watcher>async-done": { - "globals": { - "process.nextTick": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>to-object-path>kind-of": { "packages": { - "@metamask/object-multiplex>once": true, - "duplexify>end-of-stream": true, - "gulp>glob-watcher>async-done>stream-exhaust": true, - "readable-stream-2>process-nextick-args": true + "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>async-done>stream-exhaust": { - "builtin": { - "stream.Writable": true, - "util.inherits": true - }, - "globals": { - "setImmediate": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>union-value": { + "packages": { + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true, + "gulp-zip>plugin-error>arr-union": true, + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>set-value": true } }, - "gulp>glob-watcher>chokidar": { - "builtin": { - "events.EventEmitter": true, - "fs": true, - "path.basename": true, - "path.dirname": true, - "path.extname": true, - "path.join": true, - "path.relative": true, - "path.resolve": true, - "path.sep": true - }, - "globals": { - "clearTimeout": true, - "console.error": true, - "process.env.CHOKIDAR_INTERVAL": true, - "process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR": true, - "process.env.CHOKIDAR_USEPOLLING": true, - "process.nextTick": true, - "process.platform": true, - "setTimeout": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value": { "packages": { - "chokidar>normalize-path": true, - "del>is-glob": true, - "eslint>glob-parent": true, - "gulp-watch>chokidar>async-each": true, - "gulp-watch>path-is-absolute": true, - "gulp>glob-watcher>anymatch": true, - "gulp>glob-watcher>chokidar>braces": true, - "gulp>glob-watcher>chokidar>fsevents": true, - "gulp>glob-watcher>chokidar>is-binary-path": true, - "gulp>glob-watcher>chokidar>readdirp": true, - "gulp>glob-watcher>chokidar>upath": true, - "pumpify>inherits": true + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value": true } }, - "gulp>glob-watcher>chokidar>braces": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value": { "packages": { - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>repeat-element": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>snapdragon-node": true, - "gulp-watch>chokidar>braces>split-string": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp>glob-watcher>chokidar>braces>extend-shallow": true, - "gulp>glob-watcher>chokidar>braces>fill-range": true, - "gulp>gulp-cli>isobject": true, - "gulp>undertaker>arr-flatten": true + "gulp>gulp-cli>array-sort>get-value": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>has-values": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>isobject": true } }, - "gulp>glob-watcher>chokidar>braces>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>isobject": { "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>unset-value>has-value>isobject>isarray": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils": { "builtin": { - "util.inspect": true + "util": true }, "packages": { - "gulp>glob-watcher>chokidar>braces>fill-range>extend-shallow": true, - "gulp>glob-watcher>chokidar>braces>fill-range>is-number": true, - "gulp>glob-watcher>chokidar>braces>fill-range>to-regex-range": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true + "gulp-zip>plugin-error>arr-union": true, + "gulp>gulp-cli>isobject": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>extend-shallow": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend": { + "builtin": { + "util.inherits": true + }, "packages": { - "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>is-number": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy": { "packages": { - "gulp>glob-watcher>chokidar>braces>fill-range>is-number>kind-of": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy>copy-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy>kind-of": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>is-number>kind-of": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>class-utils>static-extend>object-copy>kind-of": { "packages": { "browserify>insert-module-globals>is-buffer": true } }, - "gulp>glob-watcher>chokidar>braces>fill-range>to-regex-range": { - "packages": { - "gulp>glob-watcher>chokidar>braces>fill-range>is-number": true, - "stylelint>@stylelint/postcss-markdown>remark>remark-parse>repeat-string": true - } - }, - "gulp>glob-watcher>chokidar>fsevents": { - "builtin": { - "events.EventEmitter": true, - "fs.stat": true, - "path.join": true, - "util.inherits": true - }, - "globals": { - "__dirname": true, - "console.assert": true, - "process.nextTick": true, - "process.platform": true, - "setImmediate": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>define-property": { "packages": { - "gulp-watch>chokidar>fsevents>node-pre-gyp": true + "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true } }, - "gulp>glob-watcher>chokidar>is-binary-path": { - "builtin": { - "path.extname": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep": { "packages": { - "gulp>glob-watcher>chokidar>is-binary-path>binary-extensions": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep>is-extendable": true, + "gulp>undertaker>object.reduce>for-own>for-in": true } }, - "gulp>glob-watcher>chokidar>readdirp": { - "builtin": { - "path.join": true, - "path.relative": true, - "util.inherits": true - }, - "globals": { - "setImmediate": true - }, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>mixin-deep>is-extendable": { "packages": { - "del>graceful-fs": true, - "gulp>glob-watcher>anymatch>micromatch": true, - "gulp>glob-watcher>chokidar>readdirp>readable-stream": true + "@babel/register>clone-deep>is-plain-object": true } }, - "gulp>glob-watcher>chokidar>readdirp>readable-stream": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>debug": { "builtin": { - "events.EventEmitter": true, - "stream": true, + "fs.SyncWriteStream": true, + "net.Socket": true, + "tty.WriteStream": true, + "tty.isatty": true, "util": true }, "globals": { - "process.browser": true, - "process.env.READABLE_STREAM": true, - "process.stderr": true, - "process.stdout": true, - "process.version.slice": true, - "setImmediate": true + "chrome": true, + "console": true, + "document": true, + "localStorage": true, + "navigator": true, + "process": true }, "packages": { - "gulp>glob-watcher>chokidar>readdirp>readable-stream>isarray": true, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>safe-buffer": true, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>string_decoder": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true - } - }, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>safe-buffer": { - "builtin": { - "buffer": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>debug>ms": true } }, - "gulp>glob-watcher>chokidar>readdirp>readable-stream>string_decoder": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property": { "packages": { - "gulp>glob-watcher>chokidar>readdirp>readable-stream>safe-buffer": true - } - }, - "gulp>glob-watcher>chokidar>upath": { - "builtin": { - "path": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor": true } }, - "gulp>glob-watcher>just-debounce": { - "globals": { - "clearTimeout": true, - "setTimeout": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor": { + "packages": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-accessor-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor": true, + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>kind-of": true } }, - "gulp>gulp-cli>liftoff>fined>object.pick": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-accessor-descriptor": { "packages": { - "gulp>gulp-cli>isobject": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true } }, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor": { "packages": { - "@babel/register>clone-deep>kind-of": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": true, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": true + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true } }, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-accessor-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": { "packages": { - "@babel/register>clone-deep>kind-of": true + "browserify>insert-module-globals>is-buffer": true } }, - "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor>is-data-descriptor": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>extend-shallow": { "packages": { - "@babel/register>clone-deep>kind-of": true + "gulp-watch>anymatch>micromatch>object.omit>is-extendable": true } }, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": { + "gulp>gulp-cli>matchdep>micromatch>snapdragon>use": { "packages": { - "gulp-watch>chokidar>braces>snapdragon>map-cache": true + "@babel/register>clone-deep>kind-of": true } }, - "gulp>gulp-cli>matchdep>micromatch>nanomatch": { - "builtin": { - "path.basename": true, - "path.sep": true, - "util.inspect": true - }, + "gulp>gulp-cli>matchdep>micromatch>to-regex": { "packages": { - "@babel/register>clone-deep>kind-of": true, - "gulp-watch>chokidar>braces>array-unique": true, - "gulp-watch>chokidar>braces>snapdragon": true, - "gulp-watch>chokidar>braces>to-regex": true, - "gulp-zip>plugin-error>arr-diff": true, "gulp-zip>plugin-error>extend-shallow": true, - "gulp>gulp-cli>liftoff>fined>object.pick": true, - "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, - "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": true, "gulp>gulp-cli>matchdep>micromatch>regex-not": true, - "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": true + "gulp>gulp-cli>matchdep>micromatch>to-regex>define-property": true, + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex": true } }, - "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": { + "gulp>gulp-cli>matchdep>micromatch>to-regex>define-property": { "packages": { "gulp>gulp-cli>isobject": true, "gulp>gulp-cli>matchdep>micromatch>define-property>is-descriptor": true } }, - "gulp>gulp-cli>matchdep>micromatch>regex-not": { + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex": { "packages": { - "gulp-watch>chokidar>braces>to-regex>safe-regex": true, - "gulp-zip>plugin-error>extend-shallow": true + "gulp>gulp-cli>matchdep>micromatch>to-regex>safe-regex>ret": true } }, "gulp>gulp-cli>replace-homedir>is-absolute": { @@ -6505,9 +6177,9 @@ "packages": { "del>graceful-fs": true, "gulp-sourcemaps>convert-source-map": true, + "gulp-watch>anymatch>normalize-path": true, "gulp>vinyl-fs>remove-bom-buffer": true, "gulp>vinyl-fs>vinyl-sourcemap>append-buffer": true, - "gulp>vinyl-fs>vinyl-sourcemap>normalize-path": true, "gulp>vinyl-fs>vinyl-sourcemap>now-and-later": true, "vinyl": true } @@ -6528,11 +6200,6 @@ "buffer.Buffer.isBuffer": true } }, - "gulp>vinyl-fs>vinyl-sourcemap>normalize-path": { - "packages": { - "vinyl>remove-trailing-separator": true - } - }, "gulp>vinyl-fs>vinyl-sourcemap>now-and-later": { "packages": { "@metamask/object-multiplex>once": true diff --git a/package.json b/package.json index f1c90da5fc8a..0eb9a1d6fdc8 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "attributions:generate": "./development/generate-attributions.sh" }, "resolutions": { + "chokidar": "^3.6.0", "simple-update-notifier@^1.0.0": "^2.0.0", "@babel/core": "patch:@babel/core@npm%3A7.23.2#~/.yarn/patches/@babel-core-npm-7.23.2-b93f586907.patch", "@types/react": "^16.9.53", @@ -528,7 +529,7 @@ "bify-module-groups": "^2.0.0", "browserify": "^17.0.0", "chalk": "^4.1.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "concurrently": "^8.2.2", "copy-webpack-plugin": "^12.0.2", "cross-spawn": "^7.0.3", diff --git a/yarn.lock b/yarn.lock index 9ef60d6f70e3..6658ee69abfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11499,13 +11499,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 - languageName: node - linkType: hard - "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -12116,13 +12109,6 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3": - version: 1.2.0 - resolution: "aproba@npm:1.2.0" - checksum: 10/48def777330afca699880126b555273cd9912525500edc5866b527da6fd6c54badd3ae6cc6039081e5bc22e9b349d8e65fd70f8499beb090f86aa6261e4242dd - languageName: node - linkType: hard - "archy@npm:^1.0.0": version: 1.0.0 resolution: "archy@npm:1.0.0" @@ -12137,16 +12123,6 @@ __metadata: languageName: node linkType: hard -"are-we-there-yet@npm:~1.1.2": - version: 1.1.4 - resolution: "are-we-there-yet@npm:1.1.4" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^2.0.6" - checksum: 10/86947c3ec0c8be746a0a8eb0871033bd89d8619ddbf6d51aae2d4a3a816fc06741df01307db8c0b44b9e9b20d46725fd023f1766bf586ccf1d4636cb1441452f - languageName: node - linkType: hard - "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -12495,13 +12471,6 @@ __metadata: languageName: node linkType: hard -"async-each@npm:^1.0.1": - version: 1.0.1 - resolution: "async-each@npm:1.0.1" - checksum: 10/9421203743e3379ce70defb94a78308c828e4e56d8e8bf4ba90b4c788b90a9d0759aabd327831e5a97bb6e484eccfee2f5496c1c2b239bd15f082544d919c60d - languageName: node - linkType: hard - "async-eventemitter@npm:0.2.4": version: 0.2.4 resolution: "async-eventemitter@npm:0.2.4" @@ -13092,13 +13061,6 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^1.0.0": - version: 1.11.0 - resolution: "binary-extensions@npm:1.11.0" - checksum: 10/839f346e4a1e5d43a96c5e5b3cdc608840e97b178f27a1b2fdc7b670598a23fdec42ab70d736d03db752bef44e18a57c5471e7d3ac11163deff459d6bd580e6a - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.0.0 resolution: "binary-extensions@npm:2.0.0" @@ -13372,7 +13334,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^2.3.1, braces@npm:^2.3.2": +"braces@npm:^2.3.1": version: 2.3.2 resolution: "braces@npm:2.3.2" dependencies: @@ -14219,26 +14181,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3": - version: 3.5.3 - resolution: "chokidar@npm:3.5.3" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/863e3ff78ee7a4a24513d2a416856e84c8e4f5e60efbe03e8ab791af1a183f569b62fc6f6b8044e2804966cb81277ddbbc1dc374fba3265bd609ea8efd62f5b3 - languageName: node - linkType: hard - -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.0, chokidar@npm:^3.5.3": +"chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -14257,30 +14200,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^2.0.0": - version: 2.1.8 - resolution: "chokidar@npm:2.1.8" - dependencies: - anymatch: "npm:^2.0.0" - async-each: "npm:^1.0.1" - braces: "npm:^2.3.2" - fsevents: "npm:^1.2.7" - glob-parent: "npm:^3.1.0" - inherits: "npm:^2.0.3" - is-binary-path: "npm:^1.0.0" - is-glob: "npm:^4.0.0" - normalize-path: "npm:^3.0.0" - path-is-absolute: "npm:^1.0.0" - readdirp: "npm:^2.2.1" - upath: "npm:^1.1.1" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/567c319dd2a9078fddb5a64df46163d87b104857c1b50c2ef6f9b41b3ab28867c48dbc5f0c6ddaafd3c338b147ea33a6498eb9b906c71006cba1e486a0e9350d - languageName: node - linkType: hard - -"chownr@npm:^1.1.1, chownr@npm:^1.1.4": +"chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d @@ -14970,13 +14890,6 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 10/27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb - languageName: node - linkType: hard - "consolidate@npm:^0.16.0": version: 0.16.0 resolution: "consolidate@npm:0.16.0" @@ -15702,7 +15615,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:3.X, debug@npm:^3.1.0, debug@npm:^3.1.1, debug@npm:^3.2.6, debug@npm:^3.2.7": +"debug@npm:3.X, debug@npm:^3.1.0, debug@npm:^3.1.1, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -16196,15 +16109,6 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^1.0.2": - version: 1.0.3 - resolution: "detect-libc@npm:1.0.3" - bin: - detect-libc: ./bin/detect-libc.js - checksum: 10/3849fe7720feb153e4ac9407086956e073f1ce1704488290ef0ca8aab9430a8d48c8a9f8351889e7cdc64e5b1128589501e4fef48f3a4a49ba92cd6d112d0757 - languageName: node - linkType: hard - "detect-newline@npm:^2.0.0": version: 2.1.0 resolution: "detect-newline@npm:2.1.0" @@ -19387,15 +19291,6 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^1.2.7": - version: 1.2.7 - resolution: "fs-minipass@npm:1.2.7" - dependencies: - minipass: "npm:^2.6.0" - checksum: 10/6a2d39963eaad748164530ffab49606d0f3462c7867748521af3b7039d13689be533636d50a04e8ba6bd327d4d2e899d0907f8830d1161fe2db467d59cc46dc3 - languageName: node - linkType: hard - "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -19448,17 +19343,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^1.2.7": - version: 1.2.9 - resolution: "fsevents@npm:1.2.9" - dependencies: - nan: "npm:^2.12.1" - node-pre-gyp: "npm:^0.12.0" - checksum: 10/1d8cb504a54837816badbbe7178fae84d82d2b50731571591404f13881ace67d395cc9cadda8106716f69e362b9888295fa1c3eee58c1041c2c43fc87204da36 - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -19478,16 +19362,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^1.2.7#optional!builtin": - version: 1.2.9 - resolution: "fsevents@patch:fsevents@npm%3A1.2.9#optional!builtin::version=1.2.9&hash=d11327" - dependencies: - nan: "npm:^2.12.1" - node-pre-gyp: "npm:^0.12.0" - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -19590,22 +19464,6 @@ __metadata: languageName: node linkType: hard -"gauge@npm:~2.7.3": - version: 2.7.4 - resolution: "gauge@npm:2.7.4" - dependencies: - aproba: "npm:^1.0.3" - console-control-strings: "npm:^1.0.0" - has-unicode: "npm:^2.0.0" - object-assign: "npm:^4.1.0" - signal-exit: "npm:^3.0.0" - string-width: "npm:^1.0.1" - strip-ansi: "npm:^3.0.1" - wide-align: "npm:^1.1.0" - checksum: 10/0db20a7def238f0e8eab50226247e1f94f1446ab24700eab0a56e5ccf23ce85ccf8f0c0c462112b89beb964431b1edabd3f7b31f1f6d5f62294c453594523993 - languageName: node - linkType: hard - "generic-names@npm:^2.0.1": version: 2.0.1 resolution: "generic-names@npm:2.0.1" @@ -20619,13 +20477,6 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:^2.0.0": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 10/041b4293ad6bf391e21c5d85ed03f412506d6623786b801c4ab39e4e6ca54993f13201bceb544d92963f9e0024e6e7fbf0cb1d84c9d6b31cb9c79c8c990d13d8 - languageName: node - linkType: hard - "has-value@npm:^0.3.1": version: 0.3.1 resolution: "has-value@npm:0.3.1" @@ -21162,7 +21013,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.4": +"iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -21219,15 +21070,6 @@ __metadata: languageName: node linkType: hard -"ignore-walk@npm:^3.0.1": - version: 3.0.1 - resolution: "ignore-walk@npm:3.0.1" - dependencies: - minimatch: "npm:^3.0.4" - checksum: 10/93f871229dfd2f240ad8c9eaef2496675cc31a96f6dbd15a35ddebb0f7ae16b7ca2116f1f48ae3f749badc461103b5198ae2e65206a064bba31c32f681d38571 - languageName: node - linkType: hard - "ignore@npm:^5.1.1, ignore@npm:^5.1.8, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -21575,15 +21417,6 @@ __metadata: languageName: node linkType: hard -"is-binary-path@npm:^1.0.0": - version: 1.0.1 - resolution: "is-binary-path@npm:1.0.1" - dependencies: - binary-extensions: "npm:^1.0.0" - checksum: 10/a803c99e9d898170c3b44a86fbdc0736d3d7fcbe737345433fb78e810b9fe30c982657782ad0e676644ba4693ddf05601a7423b5611423218663d6b533341ac9 - languageName: node - linkType: hard - "is-binary-path@npm:~2.1.0": version: 2.1.0 resolution: "is-binary-path@npm:2.1.0" @@ -25495,7 +25328,7 @@ __metadata: browserify: "npm:^17.0.0" chalk: "npm:^4.1.2" chart.js: "npm:^4.4.1" - chokidar: "npm:^3.5.3" + chokidar: "npm:^3.6.0" classnames: "npm:^2.2.6" concurrently: "npm:^8.2.2" contentful: "npm:^10.8.7" @@ -26045,7 +25878,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^3.0.4, micromatch@npm:^3.1.10, micromatch@npm:^3.1.4": +"micromatch@npm:^3.0.4, micromatch@npm:^3.1.4": version: 3.1.10 resolution: "micromatch@npm:3.1.10" dependencies: @@ -26308,16 +26141,6 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^2.6.0, minipass@npm:^2.9.0": - version: 2.9.0 - resolution: "minipass@npm:2.9.0" - dependencies: - safe-buffer: "npm:^5.1.2" - yallist: "npm:^3.0.0" - checksum: 10/fdd1a77996c184991f8d2ce7c5b3979bec624e2a3225e2e1e140c4038fd65873d7eb90fb29779f8733735a8827b2686f283871a0c74c908f4f7694c56fa8dadf - languageName: node - linkType: hard - "minipass@npm:^3.0.0": version: 3.3.5 resolution: "minipass@npm:3.3.5" @@ -26341,15 +26164,6 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^1.3.3": - version: 1.3.3 - resolution: "minizlib@npm:1.3.3" - dependencies: - minipass: "npm:^2.9.0" - checksum: 10/9c2c47e5687d7f896431a9b5585988ef72f848b56c6a974c9489534e8f619388d500d986ef82e1c13aedd46f3a0e81b6a88110cb1b27de7524cc8dabe8885e17 - languageName: node - linkType: hard - "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -26712,7 +26526,7 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.12.1, nan@npm:^2.13.2": +"nan@npm:^2.13.2": version: 2.15.0 resolution: "nan@npm:2.15.0" dependencies: @@ -26795,19 +26609,6 @@ __metadata: languageName: node linkType: hard -"needle@npm:^2.2.1": - version: 2.4.0 - resolution: "needle@npm:2.4.0" - dependencies: - debug: "npm:^3.2.6" - iconv-lite: "npm:^0.4.4" - sax: "npm:^1.2.4" - bin: - needle: ./bin/needle - checksum: 10/0f2de9406d07f05f89cae241594a2aa66ff0a371ead42c013942c4684f60c830d57066663a740ea6b524e7d031ec2fc8ebd9bf687c51b8936af12c5dc4f7e526 - languageName: node - linkType: hard - "negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" @@ -27024,26 +26825,6 @@ __metadata: languageName: node linkType: hard -"node-pre-gyp@npm:^0.12.0": - version: 0.12.0 - resolution: "node-pre-gyp@npm:0.12.0" - dependencies: - detect-libc: "npm:^1.0.2" - mkdirp: "npm:^0.5.1" - needle: "npm:^2.2.1" - nopt: "npm:^4.0.1" - npm-packlist: "npm:^1.1.6" - npmlog: "npm:^4.0.2" - rc: "npm:^1.2.7" - rimraf: "npm:^2.6.1" - semver: "npm:^5.3.0" - tar: "npm:^4" - bin: - node-pre-gyp: ./bin/node-pre-gyp - checksum: 10/15a25f5e4e8f9dad86b1e72ad939311923c18d270b6eec9122c81d61205227dbd6cc7dc9f0bd97101b8e6b5c88c2c154d741b9c8b017e414ae0f480782c597d0 - languageName: node - linkType: hard - "node-preload@npm:^0.2.1": version: 0.2.1 resolution: "node-preload@npm:0.2.1" @@ -27069,18 +26850,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^4.0.1": - version: 4.0.1 - resolution: "nopt@npm:4.0.1" - dependencies: - abbrev: "npm:1" - osenv: "npm:^0.1.4" - bin: - nopt: ./bin/nopt.js - checksum: 10/7e9bc2a1224fd41ec604ef8d420a5f855dd6612fa6362615f8e7e8fc44e985fb117c9acfa5cfbfd5efb081c9fb6380a4b1cf579c49d5d1cd98dd425c9c8be78b - languageName: node - linkType: hard - "nopt@npm:^7.0.0": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -27162,13 +26931,6 @@ __metadata: languageName: node linkType: hard -"npm-bundled@npm:^1.0.1": - version: 1.0.6 - resolution: "npm-bundled@npm:1.0.6" - checksum: 10/db96b54c3e27aff51055748a2e900a9808f5687037bb7247a86f9e91cbe2070426414688bf684feda24e2f20e223899fc9f754ac9c882eea9bfa32a482d589b0 - languageName: node - linkType: hard - "npm-install-checks@npm:^6.0.0": version: 6.3.0 resolution: "npm-install-checks@npm:6.3.0" @@ -27204,16 +26966,6 @@ __metadata: languageName: node linkType: hard -"npm-packlist@npm:^1.1.6": - version: 1.4.1 - resolution: "npm-packlist@npm:1.4.1" - dependencies: - ignore-walk: "npm:^3.0.1" - npm-bundled: "npm:^1.0.1" - checksum: 10/a8891f330760517152dd0b9f82fe017028e32d925985cd54672fc80c3294d4438226347b09b7307fa43dfc022d0b87117de073a4a92f5a423297d88f71af23d6 - languageName: node - linkType: hard - "npm-pick-manifest@npm:^9.0.0": version: 9.0.1 resolution: "npm-pick-manifest@npm:9.0.1" @@ -27244,18 +26996,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^4.0.2": - version: 4.1.2 - resolution: "npmlog@npm:4.1.2" - dependencies: - are-we-there-yet: "npm:~1.1.2" - console-control-strings: "npm:~1.1.0" - gauge: "npm:~2.7.3" - set-blocking: "npm:~2.0.0" - checksum: 10/b6b85c9f33da8f600f72564b6ec71136b1641b8b235fca7cc543d1041acb74c2d989d97fe443a0e65754f438d9a974a2fe1b4ff8723c78ef3f9b7a6d74b02079 - languageName: node - linkType: hard - "nth-check@npm:^2.0.1": version: 2.0.1 resolution: "nth-check@npm:2.0.1" @@ -27649,7 +27389,7 @@ __metadata: languageName: node linkType: hard -"os-homedir@npm:^1.0.0, os-homedir@npm:^1.0.1": +"os-homedir@npm:^1.0.1": version: 1.0.2 resolution: "os-homedir@npm:1.0.2" checksum: 10/af609f5a7ab72de2f6ca9be6d6b91a599777afc122ac5cad47e126c1f67c176fe9b52516b9eeca1ff6ca0ab8587fe66208bc85e40a3940125f03cdb91408e9d2 @@ -27676,23 +27416,6 @@ __metadata: languageName: node linkType: hard -"os-tmpdir@npm:^1.0.0": - version: 1.0.2 - resolution: "os-tmpdir@npm:1.0.2" - checksum: 10/5666560f7b9f10182548bf7013883265be33620b1c1b4a4d405c25be2636f970c5488ff3e6c48de75b55d02bde037249fe5dbfbb4c0fb7714953d56aed062e6d - languageName: node - linkType: hard - -"osenv@npm:^0.1.4": - version: 0.1.5 - resolution: "osenv@npm:0.1.5" - dependencies: - os-homedir: "npm:^1.0.0" - os-tmpdir: "npm:^1.0.0" - checksum: 10/779d261920f2a13e5e18cf02446484f12747d3f2ff82280912f52b213162d43d312647a40c332373cbccd5e3fb8126915d3bfea8dde4827f70f82da76e52d359 - languageName: node - linkType: hard - "outpipe@npm:^1.1.0": version: 1.1.1 resolution: "outpipe@npm:1.1.1" @@ -29552,7 +29275,7 @@ __metadata: languageName: node linkType: hard -"rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.7, 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: @@ -30258,7 +29981,7 @@ __metadata: languageName: node linkType: hard -"readable-stream-2@npm:readable-stream@^2.3.3, readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.4, readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.1.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": +"readable-stream-2@npm:readable-stream@^2.3.3, readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.4, readable-stream@npm:^2.0.5, readable-stream@npm:^2.1.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": version: 2.3.7 resolution: "readable-stream@npm:2.3.7" dependencies: @@ -30318,17 +30041,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:^2.2.1": - version: 2.2.1 - resolution: "readdirp@npm:2.2.1" - dependencies: - graceful-fs: "npm:^4.1.11" - micromatch: "npm:^3.1.10" - readable-stream: "npm:^2.0.2" - checksum: 10/14af3408ac2afa4e72e72a27e2c800d80c03e80bdef7ae4bd4b7907e98dddbeaa1ba37d4788959d9ce1131fc262cc823ce41ca9f024a91d80538241eea112c3c - languageName: node - linkType: hard - "readdirp@npm:^3.5.0, readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -31390,7 +31102,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 @@ -31686,7 +31398,7 @@ __metadata: languageName: node linkType: hard -"sax@npm:>=0.6.0, sax@npm:^1.2.4": +"sax@npm:>=0.6.0": version: 1.3.0 resolution: "sax@npm:1.3.0" checksum: 10/bb571b31d30ecb0353c2ff5f87b117a03e5fb9eb4c1519141854c1a8fbee0a77ddbe8045f413259e711833aa03da210887df8527d19cdc55f299822dbf4b34de @@ -31864,7 +31576,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.3.0, semver@npm:^5.6.0": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" bin: @@ -31987,7 +31699,7 @@ __metadata: languageName: node linkType: hard -"set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": +"set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef @@ -32872,7 +32584,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, 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.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -33603,21 +33315,6 @@ __metadata: languageName: node linkType: hard -"tar@npm:^4": - version: 4.4.19 - resolution: "tar@npm:4.4.19" - dependencies: - chownr: "npm:^1.1.4" - fs-minipass: "npm:^1.2.7" - minipass: "npm:^2.9.0" - minizlib: "npm:^1.3.3" - mkdirp: "npm:^0.5.5" - safe-buffer: "npm:^5.2.1" - yallist: "npm:^3.1.1" - checksum: 10/2715b5964578424ba5164632905a85e5a98c8dffeba657860aafa3a771b2602e6fd2a350bca891d78b8bda8cab5c53134c683ed2269b9925533477a24722e73b - languageName: node - linkType: hard - "tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -34949,13 +34646,6 @@ __metadata: languageName: node linkType: hard -"upath@npm:^1.1.1": - version: 1.2.0 - resolution: "upath@npm:1.2.0" - checksum: 10/ac07351d9e913eb7bc9bc0a17ed7d033a52575f0f2959e19726956c3e96f5d4d75aa6a7a777c4c9506e72372f58e06215e581f8dbff35611fc0a7b68ab4a6ddb - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.0.13": version: 1.0.13 resolution: "update-browserslist-db@npm:1.0.13" @@ -36080,15 +35770,6 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.0": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: "npm:^1.0.2 || 2 || 3 || 4" - checksum: 10/d5f8027b9a8255a493a94e4ec1b74a27bff6679d5ffe29316a3215e4712945c84ef73ca4045c7e20ae7d0c72f5f57f296e04a4928e773d4276a2f1222e4c2e99 - languageName: node - linkType: hard - "widest-line@npm:^2.0.0": version: 2.0.1 resolution: "widest-line@npm:2.0.1" @@ -36400,7 +36081,7 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^3.0.0, yallist@npm:^3.0.2, yallist@npm:^3.1.1": +"yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" checksum: 10/9af0a4329c3c6b779ac4736c69fae4190ac03029fa27c1aef4e6bcc92119b73dea6fe5db5fe881fb0ce2a0e9539a42cdf60c7c21eda04d1a0b8c082e38509efb From b37e39d824d35f40b07630caf9a32a0395addf8d Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 28 Jun 2024 07:59:59 +0200 Subject: [PATCH 06/15] feat: account overview for bip122:* account types (#25200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This adds a new `AccountOverview` component for the BIP122 account types. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25200?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/423 ## **Manual testing steps** 1. Start your extension: `yarn start:flask` 2. Run the bitcoin Snap + dapp: https://github.com/MetaMask/bitcoin 3. Go to: http://localhost:8000/ 4. Install the Snap and create an account 5. You should see it in the account list and you should be able to see the overview of your account ## **Screenshots/Recordings** ### **Before** ### **After** ![Screenshot 2024-06-27 at 16 03 16](https://github.com/MetaMask/metamask-extension/assets/569258/6f32a7d5-2ea1-474a-b785-16d55524e937) ![Screenshot 2024-06-27 at 16 03 21](https://github.com/MetaMask/metamask-extension/assets/569258/910ad89c-a12b-4151-91ff-0889e954ef7c) ![Screenshot 2024-06-27 at 16 03 26](https://github.com/MetaMask/metamask-extension/assets/569258/74c1a872-3376-4f38-8fca-09e21395cec6) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> --- app/_locales/en/messages.json | 3 + .../lib/accounts/BalancesController.test.ts | 16 +- .../lib/accounts/BalancesController.ts | 52 +++-- app/scripts/metamask-controller.js | 2 +- shared/constants/multichain/assets.ts | 5 + shared/constants/multichain/networks.ts | 18 ++ shared/constants/network.ts | 1 + shared/lib/multichain.test.ts | 49 +++++ shared/lib/multichain.ts | 31 +++ test/data/mock-accounts.ts | 17 +- test/setup.js | 7 + ui/components/app/asset-list/asset-list.js | 42 +++-- .../wallet-overview/btc-overview.stories.tsx | 19 ++ .../app/wallet-overview/btc-overview.test.tsx | 177 ++++++++++++++++++ .../app/wallet-overview/btc-overview.tsx | 37 ++++ .../app/wallet-overview/coin-buttons.tsx | 53 ++++-- .../app/wallet-overview/coin-overview.tsx | 33 ++-- ui/components/app/wallet-overview/index.js | 1 + .../account-list-item.test.js.snap | 58 +++--- .../account-list-item/account-list-item.js | 2 +- .../account-list-item.test.js | 6 +- .../account-overview-btc.stories.tsx | 12 ++ .../account-overview-btc.test.tsx | 43 +++++ .../account-overview/account-overview-btc.tsx | 24 +++ .../account-overview-tabs.tsx | 5 +- .../account-overview/account-overview.tsx | 23 ++- .../multichain/ramps-card/ramps-card.js | 2 +- .../receive-token-link/receive-token-link.tsx | 1 + .../token-list-item/token-list-item.js | 56 ++++-- ui/hooks/ramps/useRamps/useRamps.ts | 23 ++- ui/hooks/useCurrencyDisplay.js | 149 ++++++++++----- ui/hooks/useIsOriginalNativeTokenSymbol.js | 14 +- .../useIsOriginalNativeTokenSymbol.test.js | 8 +- ...MultichainAccountTotalFiatBalance.test.tsx | 4 +- .../useMultichainAccountTotalFiatBalance.ts | 14 +- ui/selectors/multichain.test.ts | 89 ++++++++- ui/selectors/multichain.ts | 59 ++++-- 37 files changed, 935 insertions(+), 220 deletions(-) create mode 100644 shared/lib/multichain.test.ts create mode 100644 shared/lib/multichain.ts create mode 100644 ui/components/app/wallet-overview/btc-overview.stories.tsx create mode 100644 ui/components/app/wallet-overview/btc-overview.test.tsx create mode 100644 ui/components/app/wallet-overview/btc-overview.tsx create mode 100644 ui/components/multichain/account-overview/account-overview-btc.stories.tsx create mode 100644 ui/components/multichain/account-overview/account-overview-btc.test.tsx create mode 100644 ui/components/multichain/account-overview/account-overview-btc.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6590fea16133..f50a5fa84362 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2936,6 +2936,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "The name associated with this network." }, diff --git a/app/scripts/lib/accounts/BalancesController.test.ts b/app/scripts/lib/accounts/BalancesController.test.ts index 01ce1f88c608..02627c6aa201 100644 --- a/app/scripts/lib/accounts/BalancesController.test.ts +++ b/app/scripts/lib/accounts/BalancesController.test.ts @@ -9,9 +9,10 @@ import { createMockInternalAccount } from '../../../../test/jest/mocks'; import { BalancesController, AllowedActions, - BalancesControllerEvents, + AllowedEvents, BalancesControllerState, defaultState, + BalancesControllerMessenger, } from './BalancesController'; import { Poller } from './Poller'; @@ -46,14 +47,15 @@ const setupController = ({ } = {}) => { const controllerMessenger = new ControllerMessenger< AllowedActions, - BalancesControllerEvents + AllowedEvents >(); - const balancesControllerMessenger = controllerMessenger.getRestricted({ - name: 'BalancesController', - allowedActions: ['SnapController:handleRequest'], - allowedEvents: [], - }); + const balancesControllerMessenger: BalancesControllerMessenger = + controllerMessenger.getRestricted({ + name: 'BalancesController', + allowedActions: ['SnapController:handleRequest'], + allowedEvents: ['AccountsController:stateChange'], + }); const mockSnapHandleRequest = jest.fn(); controllerMessenger.registerActionHandler( diff --git a/app/scripts/lib/accounts/BalancesController.ts b/app/scripts/lib/accounts/BalancesController.ts index eee4ac11889a..ab1eb8c6cfe6 100644 --- a/app/scripts/lib/accounts/BalancesController.ts +++ b/app/scripts/lib/accounts/BalancesController.ts @@ -12,11 +12,17 @@ import { type Balance, type CaipAssetType, type InternalAccount, + isEvmAccountType, } from '@metamask/keyring-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import type { Draft } from 'immer'; +import type { + AccountsControllerChangeEvent, + AccountsControllerState, +} from '@metamask/accounts-controller'; +import { isBtcMainnetAddress } from '../../../../shared/lib/multichain'; import { Poller } from './Poller'; const controllerName = 'BalancesController'; @@ -81,15 +87,20 @@ export type BalancesControllerEvents = BalancesControllerStateChange; */ export type AllowedActions = HandleSnapRequest; +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerChangeEvent; + /** * Messenger type for the BalancesController. */ export type BalancesControllerMessenger = RestrictedControllerMessenger< typeof controllerName, BalancesControllerActions | AllowedActions, - BalancesControllerEvents, + BalancesControllerEvents | AllowedEvents, AllowedActions['type'], - never + AllowedEvents['type'] >; /** @@ -110,21 +121,6 @@ const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0']; const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0']; export const BTC_AVG_BLOCK_TIME = 600000; // 10 minutes in milliseconds -/** - * Returns whether an address is on the Bitcoin mainnet. - * - * This function only checks the prefix of the address to determine if it's on - * the mainnet or not. It doesn't validate the address itself, and should only - * be used as a temporary solution until this information is included in the - * account object. - * - * @param address - The address to check. - * @returns `true` if the address is on the Bitcoin mainnet, `false` otherwise. - */ -function isBtcMainnet(address: string): boolean { - return address.startsWith('bc1') || address.startsWith('1'); -} - /** * The BalancesController is responsible for fetching and caching account * balances. @@ -158,6 +154,11 @@ export class BalancesController extends BaseController< }, }); + this.messagingSystem.subscribe( + 'AccountsController:stateChange', + (newState) => this.#handleOnAccountsControllerChange(newState), + ); + this.#listMultichainAccounts = listMultichainAccounts; this.#poller = new Poller(() => this.updateBalances(), BTC_AVG_BLOCK_TIME); } @@ -203,7 +204,7 @@ export class BalancesController extends BaseController< partialState.balances[account.id] = await this.#getBalances( account.id, account.metadata.snap.id, - isBtcMainnet(account.address) + isBtcMainnetAddress(account.address) ? BTC_MAINNET_ASSETS : BTC_TESTNET_ASSETS, ); @@ -216,6 +217,21 @@ export class BalancesController extends BaseController< })); } + /** + * Handles changes in the accounts state, specifically when new non-EVM accounts are added. + * + * @param newState - The new state of the accounts controller. + */ + #handleOnAccountsControllerChange(newState: AccountsControllerState) { + // If we have any new non-EVM accounts, we just update non-EVM balances + const newNonEvmAccounts = Object.values( + newState.internalAccounts.accounts, + ).filter((account) => !isEvmAccountType(account.type)); + if (newNonEvmAccounts.length) { + this.updateBalances(); + } + } + /** * Get the balances for an account. * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1799b36e6538..1be7493eca72 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -921,7 +921,7 @@ export default class MetamaskController extends EventEmitter { const multichainBalancesControllerMessenger = this.controllerMessenger.getRestricted({ name: 'BalancesController', - allowedEvents: [], + allowedEvents: ['AccountsController:stateChange'], allowedActions: ['SnapController:handleRequest'], }); diff --git a/shared/constants/multichain/assets.ts b/shared/constants/multichain/assets.ts index b4b2a74f6c22..988a9fbad624 100644 --- a/shared/constants/multichain/assets.ts +++ b/shared/constants/multichain/assets.ts @@ -3,3 +3,8 @@ import { MultichainNetworks } from './networks'; export const MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 = { BTC: `${MultichainNetworks.BITCOIN}/slip44:0`, } as const; + +export enum MultichainNativeAssets { + BITCOIN = `${MultichainNetworks.BITCOIN}/slip44:0`, + BITCOIN_TESTNET = `${MultichainNetworks.BITCOIN_TESTNET}/slip44:0`, +} diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index de5e50639374..7f629ca49c6b 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -1,12 +1,17 @@ import { ProviderConfig } from '@metamask/network-controller'; import { CaipChainId } from '@metamask/utils'; +import { isBtcMainnetAddress, isBtcTestnetAddress } from '../../lib/multichain'; export type ProviderConfigWithImageUrl = Omit & { rpcPrefs?: { imageUrl?: string }; }; export type MultichainProviderConfig = ProviderConfigWithImageUrl & { + nickname: string; chainId: CaipChainId; + // NOTE: For now we use a callback to check if the address is compatible with + // the given network or not + isAddressCompatible: (address: string) => boolean; }; export enum MultichainNetworks { @@ -34,5 +39,18 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< rpcPrefs: { imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], }, + isAddressCompatible: isBtcMainnetAddress, + }, + [MultichainNetworks.BITCOIN_TESTNET]: { + chainId: MultichainNetworks.BITCOIN_TESTNET, + rpcUrl: '', // not used + ticker: 'BTC', + nickname: 'Bitcoin (testnet)', + id: 'btc-testnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], + }, + isAddressCompatible: isBtcTestnetAddress, }, }; diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 7754b2a0f16f..955c9b2decc2 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -280,6 +280,7 @@ export const CURRENCY_SYMBOLS = { AVALANCHE: 'AVAX', BNB: 'BNB', BUSD: 'BUSD', + BTC: 'BTC', // Do we wanna mix EVM and non-EVM here? CELO: 'CELO', DAI: 'DAI', GNOSIS: 'XDAI', diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts new file mode 100644 index 000000000000..6c59f506e721 --- /dev/null +++ b/shared/lib/multichain.test.ts @@ -0,0 +1,49 @@ +import { isBtcMainnetAddress, isBtcTestnetAddress } from './multichain'; + +const MAINNET_ADDRESSES = [ + // P2WPKH + 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', + // P2PKH + '1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ', +]; + +const TESTNET_ADDRESSES = [ + // P2WPKH + 'tb1q6rmsq3vlfdhjdhtkxlqtuhhlr6pmj09y6w43g8', +]; + +const ETH_ADDRESSES = ['0x6431726EEE67570BF6f0Cf892aE0a3988F03903F']; + +describe('multichain', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(MAINNET_ADDRESSES)( + 'returns true if address is compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...TESTNET_ADDRESSES, ...ETH_ADDRESSES])( + 'returns false if address is not compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(false); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(TESTNET_ADDRESSES)( + 'returns true if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...MAINNET_ADDRESSES, ...ETH_ADDRESSES])( + 'returns false if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(false); + }, + ); +}); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts new file mode 100644 index 000000000000..07466e439266 --- /dev/null +++ b/shared/lib/multichain.ts @@ -0,0 +1,31 @@ +import { isEthAddress } from '../../app/scripts/lib/multichain/address'; + +/** + * Returns whether an address is on the Bitcoin mainnet. + * + * This function only checks the prefix of the address to determine if it's on + * the mainnet or not. It doesn't validate the address itself, and should only + * be used as a temporary solution until this information is included in the + * account object. + * + * @param address - The address to check. + * @returns `true` if the address is on the Bitcoin mainnet, `false` otherwise. + */ +export function isBtcMainnetAddress(address: string): boolean { + return ( + !isEthAddress(address) && + (address.startsWith('bc1') || address.startsWith('1')) + ); +} + +/** + * Returns whether an address is on the Bitcoin testnet. + * + * See {@link isBtcMainnetAddress} for implementation details. + * + * @param address - The address to check. + * @returns `true` if the address is on the Bitcoin testnet, `false` otherwise. + */ +export function isBtcTestnetAddress(address: string): boolean { + return !isEthAddress(address) && !isBtcMainnetAddress(address); +} diff --git a/test/data/mock-accounts.ts b/test/data/mock-accounts.ts index ff6009ebd555..2273915f7a5f 100644 --- a/test/data/mock-accounts.ts +++ b/test/data/mock-accounts.ts @@ -40,7 +40,7 @@ export const MOCK_ACCOUNT_ERC4337: InternalAccount = { export const MOCK_ACCOUNT_BIP122_P2WPKH: InternalAccount = { id: 'ae247df6-3911-47f7-9e36-28e6a7d96078', - address: 'bc1qaabb', + address: 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', options: {}, methods: [BtcMethod.SendMany], type: BtcAccountType.P2wpkh, @@ -52,8 +52,23 @@ export const MOCK_ACCOUNT_BIP122_P2WPKH: InternalAccount = { }, }; +export const MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET: InternalAccount = { + id: 'fcdafe8b-4bdf-4e25-9051-e255b2a0af5f', + address: 'tb1q6rmsq3vlfdhjdhtkxlqtuhhlr6pmj09y6w43g8', + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, + metadata: { + name: 'Bitcoin Testnet Account', + keyring: { type: KeyringTypes.snap }, + importTime: 1691565967600, + lastSelected: 1955565967656, + }, +}; + export const MOCK_ACCOUNTS = { [MOCK_ACCOUNT_EOA.id]: MOCK_ACCOUNT_EOA, [MOCK_ACCOUNT_ERC4337.id]: MOCK_ACCOUNT_ERC4337, [MOCK_ACCOUNT_BIP122_P2WPKH.id]: MOCK_ACCOUNT_BIP122_P2WPKH, + [MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET.id]: MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, }; diff --git a/test/setup.js b/test/setup.js index f96e744a917a..17f9c45249ab 100644 --- a/test/setup.js +++ b/test/setup.js @@ -7,3 +7,10 @@ window.SVGPathElement = window.SVGPathElement || { prototype: {} }; global.indexedDB = {}; // scrollIntoView is not available in JSDOM window.HTMLElement.prototype.scrollIntoView = () => undefined + +global.platform = { + // Required for: coin overviews components + openTab: () => undefined, + // Required for: settings info tab + getVersion: () => '', +}; diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 959789b8eebc..8e493037da44 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -5,7 +5,6 @@ import TokenList from '../token-list'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'; import { - getSelectedAccountCachedBalance, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getShouldHideZeroBalanceTokens, @@ -19,6 +18,7 @@ import { getMultichainShouldShowFiat, getMultichainCurrencyImage, getMultichainIsMainnet, + getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -48,9 +48,8 @@ import { import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF -const AssetList = ({ onClickAsset }) => { +const AssetList = ({ onClickAsset, showTokensLinks }) => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const selectedAccountBalance = useSelector(getSelectedAccountCachedBalance); const nativeCurrency = useSelector(getMultichainNativeCurrency); const showFiat = useSelector(getMultichainShouldShowFiat); const isMainnet = useSelector(getMultichainIsMainnet); @@ -65,7 +64,7 @@ const AssetList = ({ onClickAsset }) => { rpcUrl, ); const trackEvent = useContext(MetaMetricsContext); - const balance = useSelector(getSelectedAccountCachedBalance); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); const balanceIsLoading = !balance; const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( @@ -82,13 +81,13 @@ const AssetList = ({ onClickAsset }) => { } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }); const [primaryCurrencyDisplay, primaryCurrencyProperties] = - useCurrencyDisplay(selectedAccountBalance, { + useCurrencyDisplay(balance, { numberOfDecimals: primaryNumberOfDecimals, currency: primaryCurrency, }); const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = - useCurrencyDisplay(selectedAccountBalance, { + useCurrencyDisplay(balance, { numberOfDecimals: secondaryNumberOfDecimals, currency: secondaryCurrency, }); @@ -113,6 +112,10 @@ const AssetList = ({ onClickAsset }) => { const isEvm = useSelector(getMultichainIsEvm); + // NOTE: Since we can parametrize it now, we keep the original behavior + // for EVM assets + const shouldShowTokensLinks = showTokensLinks ?? isEvm; + let isStakeable = isMainnet && isEvm; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) isStakeable = false; @@ -181,18 +184,22 @@ const AssetList = ({ onClickAsset }) => { }); }} /> - {balanceIsZero && ( - 0 ? 0 : 4} - /> + {shouldShowTokensLinks && ( + <> + {balanceIsZero && ( + 0 ? 0 : 4} + /> + )} + 0 && !balanceIsZero ? 0 : 2} + /> + )} - 0 && !balanceIsZero ? 0 : 2} - /> {showDetectedTokens && ( )} @@ -202,6 +209,7 @@ const AssetList = ({ onClickAsset }) => { AssetList.propTypes = { onClickAsset: PropTypes.func.isRequired, + showTokensLinks: PropTypes.bool, }; export default AssetList; diff --git a/ui/components/app/wallet-overview/btc-overview.stories.tsx b/ui/components/app/wallet-overview/btc-overview.stories.tsx new file mode 100644 index 000000000000..43dff2554bef --- /dev/null +++ b/ui/components/app/wallet-overview/btc-overview.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import BtcOverview from './btc-overview'; + +export default { + title: 'Components/App/WalletOverview/BtcOverview', + component: BtcOverview, + parameters: { + docs: { + description: { + component: + 'A component that displays an overview of Bitcoin wallet information.', + }, + }, + }, +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx new file mode 100644 index 000000000000..233096f0918c --- /dev/null +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent } from '@testing-library/react'; +import thunk from 'redux-thunk'; +import { Cryptocurrency } from '@metamask/assets-controllers'; +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { MultichainNativeAssets } from '../../../../shared/constants/multichain/assets'; +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; +import { RampsMetaMaskEntry } from '../../../hooks/ramps/useRamps/useRamps'; +import BtcOverview from './btc-overview'; + +const PORTOFOLIO_URL = 'https://portfolio.test'; + +const BTC_OVERVIEW_BUY = 'coin-overview-buy'; +const BTC_OVERVIEW_BRIDGE = 'coin-overview-bridge'; +const BTC_OVERVIEW_PORTFOLIO = 'coin-overview-portfolio'; +const BTC_OVERVIEW_SWAP = 'token-overview-button-swap'; +const BTC_OVERVIEW_SEND = 'coin-overview-send'; +const BTC_OVERVIEW_PRIMARY_CURRENCY = 'coin-overview__primary-currency'; + +const mockMetaMetricsId = 'deadbeef'; +const mockNonEvmBalance = '1'; +const mockNonEvmAccount = { + address: 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', + id: '542490c8-d178-433b-9f31-f680b11f45a5', + metadata: { + name: 'Bitcoin Account', + keyring: { + type: 'Snap Keyring', + }, + snap: { + id: 'btc-snap-id', + name: 'btc-snap-name', + }, + }, + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, +}; + +function getStore(state?: Record) { + return configureMockStore([thunk])({ + metamask: { + ...mockState.metamask, + internalAccounts: { + accounts: { + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + // (Multichain) BalancesController + balances: { + [mockNonEvmAccount.id]: { + [MultichainNativeAssets.BITCOIN]: { + amount: mockNonEvmBalance, + unit: 'BTC', + }, + }, + }, + // (Multichain) RatesController + fiatCurrency: 'usd', + rates: { + [Cryptocurrency.Btc]: { + conversionRate: '1.000', + conversionDate: 0, + }, + }, + cryptocurrencies: [Cryptocurrency.Btc], + // Required, during onboarding, the extension will assume we're in an "EVM context", meaning + // most multichain selectors will not use non-EVM logic despite having a non-EVM + // selected account + completedOnboarding: true, + // Used when clicking on some buttons + metaMetricsId: mockMetaMetricsId, + // Override state if provided + ...state, + }, + }); +} + +function makePortfolioUrl(path: string, getParams: Record) { + const params = new URLSearchParams(getParams); + return `${PORTOFOLIO_URL}/${path}?${params.toString()}`; +} + +describe('BtcOverview', () => { + it('shows the primary balance', async () => { + const { queryByTestId, queryByText } = renderWithProvider( + , + getStore(), + ); + + const primaryBalance = queryByTestId(BTC_OVERVIEW_PRIMARY_CURRENCY); + expect(primaryBalance).toBeInTheDocument(); + expect(primaryBalance).toHaveTextContent(`${mockNonEvmBalance}BTC`); + // For now we consider balance to be always cached + expect(queryByText('*')).toBeInTheDocument(); + }); + + it('shows a spinner if balance is not available', async () => { + const { container } = renderWithProvider( + , + getStore({ + // The balances won't be available + balances: {}, + }), + ); + + const spinner = container.querySelector( + '.coin-overview__balance .coin-overview__primary-container .spinner', + ); + expect(spinner).toBeInTheDocument(); + }); + + it('buttons Send/Swap/Bridge are disabled', () => { + const { queryByTestId } = renderWithProvider(, getStore()); + + for (const buttonTestId of [ + BTC_OVERVIEW_SEND, + BTC_OVERVIEW_SWAP, + BTC_OVERVIEW_BRIDGE, + ]) { + const button = queryByTestId(buttonTestId); + expect(button).toBeInTheDocument(); + expect(button).toBeDisabled(); + } + }); + + it('shows the "Buy & Sell" button', () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + expect(buyButton).toBeInTheDocument(); + }); + + it('opens the Portfolio "Buy & Sell" URI when clicking on "Buy & Sell" button', async () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + expect(buyButton).toBeInTheDocument(); + fireEvent.click(buyButton as HTMLElement); + + expect(openTabSpy).toHaveBeenCalledTimes(1); + expect(openTabSpy).toHaveBeenCalledWith({ + url: makePortfolioUrl('buy', { + metamaskEntry: RampsMetaMaskEntry.BuySellButton, + chainId: MultichainNetworks.BITCOIN, + metametricsId: mockMetaMetricsId, + }), + }); + }); + + it('always show the Portfolio button', () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const portfolioButton = queryByTestId(BTC_OVERVIEW_PORTFOLIO); + expect(portfolioButton).toBeInTheDocument(); + }); + + it('open the Portfolio URI when clicking on Portfolio button', async () => { + const { queryByTestId } = renderWithProvider(, getStore()); + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + + const portfolioButton = queryByTestId(BTC_OVERVIEW_PORTFOLIO); + expect(portfolioButton).toBeInTheDocument(); + fireEvent.click(portfolioButton as HTMLElement); + + expect(openTabSpy).toHaveBeenCalledTimes(1); + expect(openTabSpy).toHaveBeenCalledWith({ + url: makePortfolioUrl('', { + metamaskEntry: 'ext_portfolio_button', + metametricsId: mockMetaMetricsId, + }), + }); + }); +}); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx new file mode 100644 index 000000000000..3703252f205a --- /dev/null +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { useSelector } from 'react-redux'; +import { + getMultichainProviderConfig, + getMultichainSelectedAccountCachedBalance, +} from '../../../selectors/multichain'; +import { CoinOverview } from './coin-overview'; + +type BtcOverviewProps = { + className?: string; +}; + +const BtcOverview = ({ className }: BtcOverviewProps) => { + const { chainId } = useSelector(getMultichainProviderConfig); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + + return ( + + ); +}; + +export default BtcOverview; diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 8eb24128b60e..b1774897edc4 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -6,9 +6,20 @@ import { useLocation, ///: END:ONLY_INCLUDE_IF } from 'react-router-dom'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { toHex } from '@metamask/controller-utils'; +///: END:ONLY_INCLUDE_IF +import { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + isCaipChainId, + ///: END:ONLY_INCLUDE_IF + CaipChainId, +} from '@metamask/utils'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { ChainId } from '../../../../shared/constants/network'; +///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import { CaipChainId } from '@metamask/utils'; import { getMmiPortfolioEnabled, getMmiPortfolioUrl, @@ -22,11 +33,9 @@ import { SEND_ROUTE, } from '../../../helpers/constants/routes'; import { - SwapsEthToken, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + SwapsEthToken, getCurrentKeyring, - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getMetaMetricsId, ///: END:ONLY_INCLUDE_IF getUseExternalServices, @@ -66,17 +75,23 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain, isBuyableChain, + // TODO: Remove this logic once `isNativeTokenBuyable` has been + // merged (see: https://github.com/MetaMask/metamask-extension/pull/24041) + isBuyableChainWithoutSigning = false, defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix = 'coin', }: { - classPrefix?: string; - isBuyableChain: boolean; - isSigningEnabled: boolean; + chainId: `0x${string}` | CaipChainId | number; isSwapsChain: boolean; + isSigningEnabled: boolean; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain: boolean; - chainId: `0x${string}` | CaipChainId | number; + isBuyableChain: boolean; + isBuyableChainWithoutSigning?: boolean; defaultSwapsToken?: SwapsEthToken; + ///: END:ONLY_INCLUDE_IF + classPrefix?: string; }) => { const t = useContext(I18nContext); const dispatch = useDispatch(); @@ -97,7 +112,10 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) { condition: !isBuyableChain, message: '' }, ///: END:ONLY_INCLUDE_IF - { condition: !isSigningEnabled, message: 'methodNotSupported' }, + { + condition: !(isSigningEnabled || isBuyableChainWithoutSigning), + message: 'methodNotSupported', + }, ], sendButton: [ { condition: !isSigningEnabled, message: 'methodNotSupported' }, @@ -130,6 +148,16 @@ const CoinButtons = ({ return contents; }; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const getChainId = (): CaipChainId | ChainId => { + if (isCaipChainId(chainId)) { + return chainId as CaipChainId; + } + // Otherwise we assume that's an EVM chain ID, so use the usual 0x prefix + return toHex(chainId) as ChainId; + }; + ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled); const mmiPortfolioUrl = useSelector(getMmiPortfolioUrl); @@ -248,7 +276,7 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const handleBuyAndSellOnClick = useCallback(() => { - openBuyCryptoInPdapp(); + openBuyCryptoInPdapp(getChainId()); trackEvent({ event: MetaMetricsEventName.NavBuyButtonClicked, category: MetaMetricsEventCategory.Navigation, @@ -311,7 +339,10 @@ const CoinButtons = ({ Icon={ } - disabled={!isBuyableChain || !isSigningEnabled} + disabled={ + !isBuyableChain || + !(isSigningEnabled || isBuyableChainWithoutSigning) + } data-testid={`${classPrefix}-overview-buy`} label={t('buyAndSell')} onClick={handleBuyAndSellOnClick} diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index abf1f6ca7158..6364b0231e82 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -3,15 +3,13 @@ import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { zeroAddress } from 'ethereumjs-util'; -///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { CaipChainId } from '@metamask/utils'; -///: END:ONLY_INCLUDE_IF +import type { Hex } from '@metamask/utils'; import { I18nContext } from '../../../contexts/i18n'; import Tooltip from '../../ui/tooltip'; import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import { - getShouldShowFiat, getPreferences, getTokensMarketData, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -20,24 +18,28 @@ import { } from '../../../selectors'; import Spinner from '../../ui/spinner'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; import { showPrimaryCurrency } from '../../../../shared/modules/currency-display.utils'; import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; +import { + getMultichainIsEvm, + getMultichainProviderConfig, + getMultichainShouldShowFiat, +} from '../../../selectors/multichain'; import WalletOverview from './wallet-overview'; import CoinButtons from './coin-buttons'; export type CoinOverviewProps = { balance: string; balanceIsCached: boolean; - className: string; - classPrefix: string; - chainId: CaipChainId | number; - showAddress: boolean; + className?: string; + classPrefix?: string; + chainId: CaipChainId | Hex; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) // FIXME: This seems to be for Ethereum only defaultSwapsToken?: SwapsEthToken; isBridgeChain: boolean; isBuyableChain: boolean; + isBuyableChainWithoutSigning: boolean; ///: END:ONLY_INCLUDE_IF isSwapsChain: boolean; isSigningEnabled: boolean; @@ -53,6 +55,7 @@ export const CoinOverview = ({ defaultSwapsToken, isBridgeChain, isBuyableChain, + isBuyableChainWithoutSigning, ///: END:ONLY_INCLUDE_IF isSwapsChain, isSigningEnabled, @@ -65,9 +68,10 @@ export const CoinOverview = ({ ///: END:ONLY_INCLUDE_IF const t = useContext(I18nContext); - const showFiat = useSelector(getShouldShowFiat); + const isEvm = useSelector(getMultichainIsEvm); + const showFiat = useSelector(getMultichainShouldShowFiat); const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const { ticker, type, rpcUrl } = useSelector(getProviderConfig); + const { ticker, type, rpcUrl } = useSelector(getMultichainProviderConfig); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, ticker, @@ -131,9 +135,11 @@ export const CoinOverview = ({ hideTitle /> )} - + {isEvm && ( + + )} } @@ -146,6 +152,7 @@ export const CoinOverview = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain, isBuyableChain, + isBuyableChainWithoutSigning, defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix, diff --git a/ui/components/app/wallet-overview/index.js b/ui/components/app/wallet-overview/index.js index 2eb058f81afd..54536007bc41 100644 --- a/ui/components/app/wallet-overview/index.js +++ b/ui/components/app/wallet-overview/index.js @@ -1 +1,2 @@ export { default as EthOverview } from './eth-overview'; +export { default as BtcOverview } from './btc-overview'; 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 d97d0fd70fbe..0b2b13a29277 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 @@ -32,7 +32,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam class="mm-avatar-account__jazzicon" >
- 0x0DCD5...3E7bc + bc1qn3s...5eker

{ const { container } = render({ account: mockNonEvmAccount }); expect(screen.getByText(mockAccount.metadata.name)).toBeInTheDocument(); expect( - screen.getByText( - shortenAddress(toChecksumHexAddress(mockAccount.address)), - ), + screen.getByText(shortenAddress(mockNonEvmAccount.address)), ).toBeInTheDocument(); expect( document.querySelector('[title="$100,000.00 USD"]'), diff --git a/ui/components/multichain/account-overview/account-overview-btc.stories.tsx b/ui/components/multichain/account-overview/account-overview-btc.stories.tsx new file mode 100644 index 000000000000..2afc54e22b23 --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-btc.stories.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { AccountOverviewBtc } from './account-overview-btc' +import { AccountOverviewCommonProps } from './common'; + +export default { + title: 'Components/Multichain/AccountOverviewBtc', + component: AccountOverviewBtc, +}; + +export const DefaultStory = ( + args: JSX.IntrinsicAttributes & AccountOverviewCommonProps +) => ; diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-btc.test.tsx new file mode 100644 index 000000000000..f75e906e0bce --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-btc.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import { + AccountOverviewBtc, + AccountOverviewBtcProps, +} from './account-overview-btc'; + +const defaultProps: AccountOverviewBtcProps = { + defaultHomeActiveTabName: '', + onTabClick: jest.fn(), + setBasicFunctionalityModalOpen: jest.fn(), + onSupportLinkClick: jest.fn(), +}; + +const render = (props: AccountOverviewBtcProps = defaultProps) => { + const store = configureStore({ + metamask: mockState.metamask, + }); + + return renderWithProvider(, store); +}; + +describe('AccountOverviewBtc', () => { + it('shows only Tokens and Activity tabs', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('account-overview__asset-tab')).toBeInTheDocument(); + expect(queryByTestId('account-overview__nfts-tab')).not.toBeInTheDocument(); + expect(queryByTestId('account-overview__activity-tab')).toBeInTheDocument(); + }); + + it('does not show tokens links', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('account-overview__asset-tab')).toBeInTheDocument(); + expect(queryByTestId('receive-token-button')).not.toBeInTheDocument(); + expect(queryByTestId('import-token-button')).not.toBeInTheDocument(); + // TODO: This one might be required, but we do not really handle tokens for BTC yet... + expect(queryByTestId('refresh-list-button')).not.toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-overview/account-overview-btc.tsx b/ui/components/multichain/account-overview/account-overview-btc.tsx new file mode 100644 index 000000000000..dd58b2eef414 --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-btc.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { BtcOverview } from '../../app/wallet-overview'; +import { AccountOverviewLayout } from './account-overview-layout'; +import { AccountOverviewCommonProps } from './common'; + +export type AccountOverviewBtcProps = AccountOverviewCommonProps; + +export const AccountOverviewBtc = (props: AccountOverviewBtcProps) => { + return ( + + { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) + + ///: END:ONLY_INCLUDE_IF + } + + ); +}; diff --git a/ui/components/multichain/account-overview/account-overview-tabs.tsx b/ui/components/multichain/account-overview/account-overview-tabs.tsx index 284bfb8223f3..543675d06201 100644 --- a/ui/components/multichain/account-overview/account-overview-tabs.tsx +++ b/ui/components/multichain/account-overview/account-overview-tabs.tsx @@ -41,6 +41,7 @@ import { AccountOverviewCommonProps } from './common'; export type AccountOverviewTabsProps = AccountOverviewCommonProps & { showTokens: boolean; + showTokensLinks?: boolean; showNfts: boolean; showActivity: boolean; }; @@ -52,6 +53,7 @@ export const AccountOverviewTabs = ({ ///: END:ONLY_INCLUDE_IF defaultHomeActiveTabName, showTokens, + showTokensLinks, showNfts, showActivity, }: AccountOverviewTabsProps) => { @@ -141,7 +143,8 @@ export const AccountOverviewTabs = ({ > + showTokensLinks={showTokensLinks ?? true} + onClickAsset={(asset: string) => history.push(`${ASSET_ROUTE}/${asset}`) } /> diff --git a/ui/components/multichain/account-overview/account-overview.tsx b/ui/components/multichain/account-overview/account-overview.tsx index 320b865478fc..3d6121e41471 100644 --- a/ui/components/multichain/account-overview/account-overview.tsx +++ b/ui/components/multichain/account-overview/account-overview.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { useSelector } from 'react-redux'; +import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { BannerAlert, BannerAlertSeverity } from '../../component-library'; -import { isSelectedInternalAccountEth } from '../../../selectors/accounts'; +import { getSelectedInternalAccount } from '../../../selectors'; import { AccountOverviewEth } from './account-overview-eth'; +import { AccountOverviewBtc } from './account-overview-btc'; import { AccountOverviewUnknown } from './account-overview-unknown'; import { AccountOverviewCommonProps } from './common'; @@ -13,12 +15,22 @@ export type AccountOverviewProps = AccountOverviewCommonProps & { export function AccountOverview(props: AccountOverviewProps) { const t = useI18nContext(); - - const isEth = useSelector(isSelectedInternalAccountEth); - const isUnknown = !isEth; + const account = useSelector(getSelectedInternalAccount); const { useExternalServices, setBasicFunctionalityModalOpen } = props; + const renderAccountOverviewOption = () => { + switch (account.type) { + case EthAccountType.Eoa: + case EthAccountType.Erc4337: + return ; + case BtcAccountType.P2wpkh: + return ; + default: + return ; + } + }; + return ( <> {!useExternalServices && ( @@ -33,8 +45,7 @@ export function AccountOverview(props: AccountOverviewProps) { title={t('basicConfigurationBannerTitle')} /> )} - {isEth && } - {isUnknown && } + {renderAccountOverviewOption()} ); } diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index 2fa793b7e958..c03e9e956f9a 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -89,7 +89,7 @@ export const RampsCard = ({ variant }) => { }, [currentLocale, chainId, nickname, trackEvent]); const onClick = useCallback(() => { - openBuyCryptoInPdapp(); + openBuyCryptoInPdapp(chainId); trackEvent({ event: MetaMetricsEventName.NavBuyButtonClicked, category: MetaMetricsEventCategory.Navigation, diff --git a/ui/components/multichain/receive-token-link/receive-token-link.tsx b/ui/components/multichain/receive-token-link/receive-token-link.tsx index fa8079238b0f..41416833b5aa 100644 --- a/ui/components/multichain/receive-token-link/receive-token-link.tsx +++ b/ui/components/multichain/receive-token-link/receive-token-link.tsx @@ -53,6 +53,7 @@ export const ReceiveTokenLink: React.FC> = ({ )} { diff --git a/ui/components/multichain/token-list-item/token-list-item.js b/ui/components/multichain/token-list-item/token-list-item.js index 24fb218d83ba..a52ad8b77cad 100644 --- a/ui/components/multichain/token-list-item/token-list-item.js +++ b/ui/components/multichain/token-list-item/token-list-item.js @@ -36,14 +36,17 @@ import { import { ModalContent } from '../../component-library/modal-content/deprecated'; import { ModalHeader } from '../../component-library/modal-header/deprecated'; import { - getCurrentChainId, getMetaMetricsId, - getNativeCurrencyImage, getPreferences, getTestNetworkBackgroundColor, getTokensMarketData, } from '../../../selectors'; -import { getMultichainCurrentNetwork } from '../../../selectors/multichain'; +import { + getMultichainCurrentChainId, + getMultichainCurrentNetwork, + getMultichainIsEvm, + getMultichainNativeCurrencyImage, +} from '../../../selectors/multichain'; import Tooltip from '../../ui/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -76,10 +79,11 @@ export const TokenListItem = ({ address = null, }) => { const t = useI18nContext(); - const primaryTokenImage = useSelector(getNativeCurrencyImage); + const isEvm = useSelector(getMultichainIsEvm); + const primaryTokenImage = useSelector(getMultichainNativeCurrencyImage); const trackEvent = useContext(MetaMetricsContext); const metaMetricsId = useSelector(getMetaMetricsId); - const chainId = useSelector(getCurrentChainId); + const chainId = useSelector(getMultichainCurrentChainId); // Scam warning const showScamWarning = isNativeCurrency && !isOriginalTokenSymbol; @@ -92,15 +96,27 @@ export const TokenListItem = ({ const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; const history = useHistory(); + const getTokenTitle = () => { + if (!isOriginalTokenSymbol) { + return title; + } + // We only consider native token symbols! + switch (title) { + case CURRENCY_SYMBOLS.ETH: + return t('networkNameEthereum'); + case CURRENCY_SYMBOLS.BTC: + return t('networkNameBitcoin'); + default: + return title; + } + }; + const tokensMarketData = useSelector(getTokensMarketData); const tokenPercentageChange = tokensMarketData?.[address]?.pricePercentChange1d; - const tokenTitle = - title === CURRENCY_SYMBOLS.ETH && isOriginalTokenSymbol - ? t('networkNameEthereum') - : title; + const tokenTitle = getTokenTitle(); const stakeableTitle = ( )} - + {isEvm && ( + + )} {showScamWarning ? ( @@ -351,7 +371,7 @@ export const TokenListItem = ({ > - {showScamWarningModal ? ( + {isEvm && showScamWarningModal ? ( diff --git a/ui/hooks/ramps/useRamps/useRamps.ts b/ui/hooks/ramps/useRamps/useRamps.ts index 7219d5fe4193..aa8ccb1c76d2 100644 --- a/ui/hooks/ramps/useRamps/useRamps.ts +++ b/ui/hooks/ramps/useRamps/useRamps.ts @@ -1,12 +1,12 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import type { Hex } from '@metamask/utils'; +import { CaipChainId } from '@metamask/utils'; import { ChainId } from '../../../../shared/constants/network'; import { getCurrentChainId, getMetaMetricsId } from '../../../selectors'; type IUseRamps = { - openBuyCryptoInPdapp: VoidFunction; - getBuyURI: (chainId: ChainId) => string; + openBuyCryptoInPdapp: (chainId?: ChainId | CaipChainId) => void; + getBuyURI: (chainId: ChainId | CaipChainId) => string; }; export enum RampsMetaMaskEntry { @@ -24,7 +24,7 @@ const useRamps = ( const metaMetricsId = useSelector(getMetaMetricsId); const getBuyURI = useCallback( - (_chainId: Hex) => { + (_chainId: ChainId | CaipChainId) => { const params = new URLSearchParams(); params.set('metamaskEntry', metamaskEntry); params.set('chainId', _chainId); @@ -36,12 +36,15 @@ const useRamps = ( [metaMetricsId], ); - const openBuyCryptoInPdapp = useCallback(() => { - const buyUrl = getBuyURI(chainId); - global.platform.openTab({ - url: buyUrl, - }); - }, [chainId]); + const openBuyCryptoInPdapp = useCallback( + (_chainId?: ChainId | CaipChainId) => { + const buyUrl = getBuyURI(_chainId || chainId); + global.platform.openTab({ + url: buyUrl, + }); + }, + [chainId], + ); return { openBuyCryptoInPdapp, getBuyURI }; }; diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index f8c313118554..a7798d5ff10e 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -1,18 +1,18 @@ import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import BigNumber from 'bignumber.js'; import { formatCurrency } from '../helpers/utils/confirm-tx.util'; import { getMultichainCurrentCurrency, getMultichainIsEvm, getMultichainNativeCurrency, + getMultichainConversionRate, } from '../selectors/multichain'; -import { getConversionRate } from '../ducks/metamask/metamask'; import { getValueFromWeiHex } from '../../shared/modules/conversion.utils'; import { TEST_NETWORK_TICKER_MAP } from '../../shared/constants/network'; import { Numeric } from '../../shared/modules/Numeric'; import { EtherDenomination } from '../../shared/constants/common'; +import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { useMultichainSelector } from './useMultichainSelector'; // The smallest non-zero amount that can be displayed. @@ -26,6 +26,74 @@ const MIN_AMOUNT_DISPLAY = `<${MIN_AMOUNT}`; // It set to the number of decimal places in the minimum amount. export const DEFAULT_PRECISION = new BigNumber(MIN_AMOUNT).decimalPlaces(); +function formatEthCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + nativeCurrency, + inputValue, + conversionRate, + denomination, + numberOfDecimals, +}) { + if (isNativeCurrency || (!isUserPreferredCurrency && !nativeCurrency)) { + const ethDisplayValue = new Numeric(inputValue, 16, EtherDenomination.WEI) + .toDenomination(denomination || EtherDenomination.ETH) + .round(numberOfDecimals || DEFAULT_PRECISION) + .toBase(10) + .toString(); + + return ethDisplayValue === '0' && inputValue && Number(inputValue) !== 0 + ? MIN_AMOUNT_DISPLAY + : ethDisplayValue; + } else if (isUserPreferredCurrency && conversionRate) { + return formatCurrency( + getValueFromWeiHex({ + value: inputValue, + fromCurrency: nativeCurrency, + toCurrency: currency, + conversionRate, + numberOfDecimals: numberOfDecimals || 2, + toDenomination: denomination, + }), + currency, + ); + } + return null; +} + +function formatBtcCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + currentCurrency, + nativeCurrency, + inputValue, + conversionRate, +}) { + if (isNativeCurrency || (!isUserPreferredCurrency && !nativeCurrency)) { + // NOTE: We use the value coming from the BalancesController here (and thus, the non-EVM + // account Snap). + // We use `Numeric` here, so we handle those amount the same way than for EVMs (it's worth + // noting that if `inputValue` is not properly defined, the amount will be set to '0', see + // `Numeric` constructor for that) + return new Numeric(inputValue, 10).toString(); // BTC usually uses 10 digits + } else if (isUserPreferredCurrency && conversionRate) { + const amount = + getTokenFiatAmount( + 1, // coin to native conversion rate is 1:1 + Number(conversionRate), // native to fiat conversion rate + currentCurrency, + inputValue, + 'BTC', + false, + false, + ) ?? '0'; // if the conversion fails, return 0 + return formatCurrency(amount, currency); + } + return null; +} + /** * Defines the shape of the options parameter for useCurrencyDisplay * @@ -79,60 +147,53 @@ export function useCurrencyDisplay( getMultichainNativeCurrency, account, ); - const conversionRate = useSelector(getConversionRate); + const conversionRate = useMultichainSelector( + getMultichainConversionRate, + account, + ); const isUserPreferredCurrency = currency === currentCurrency; + const isNativeCurrency = currency === nativeCurrency; const value = useMemo(() => { if (displayValue) { return displayValue; } - if (isEvm) { - if ( - currency === nativeCurrency || - (!isUserPreferredCurrency && !nativeCurrency) - ) { - const ethDisplayValue = new Numeric( - inputValue, - 16, - EtherDenomination.WEI, - ) - .toDenomination(denomination || EtherDenomination.ETH) - .round(numberOfDecimals || DEFAULT_PRECISION) - .toBase(10) - .toString(); - - return ethDisplayValue === '0' && inputValue && Number(inputValue) !== 0 - ? MIN_AMOUNT_DISPLAY - : ethDisplayValue; - } else if (isUserPreferredCurrency && conversionRate) { - return formatCurrency( - getValueFromWeiHex({ - value: inputValue, - fromCurrency: nativeCurrency, - toCurrency: currency, - conversionRate, - numberOfDecimals: numberOfDecimals || 2, - toDenomination: denomination, - }), - currency, - ); - } - } else { - // For non-EVM we assume the input value can be formatted "as-is" - return formatCurrency(inputValue, currency); + if (!isEvm) { + // TODO: We would need to update this for other non-EVM coins + return formatBtcCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + currentCurrency, + nativeCurrency, + inputValue, + conversionRate, + }); } - return null; + + return formatEthCurrencyDisplay({ + isNativeCurrency, + isUserPreferredCurrency, + currency, + nativeCurrency, + inputValue, + conversionRate, + denomination, + numberOfDecimals, + }); }, [ - inputValue, + displayValue, + isEvm, + isNativeCurrency, + isUserPreferredCurrency, + currency, nativeCurrency, + inputValue, conversionRate, - displayValue, - numberOfDecimals, denomination, - currency, - isUserPreferredCurrency, - isEvm, + numberOfDecimals, + currentCurrency, ]); let suffix; diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.js b/ui/hooks/useIsOriginalNativeTokenSymbol.js index 47035c3fbb6e..06dc380d861d 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.js @@ -7,13 +7,17 @@ import { } from '../../shared/constants/network'; import { DAY } from '../../shared/constants/time'; import { useSafeChainsListValidationSelector } from '../selectors'; +import { + getMultichainIsEvm, + getMultichainCurrentNetwork, +} from '../selectors/multichain'; import { getValidUrl } from '../../app/scripts/lib/util'; export function useIsOriginalNativeTokenSymbol( chainId, ticker, type, - rpcUrl = null, + rpcUrl = '', ) { const [isOriginalNativeSymbol, setIsOriginalNativeSymbol] = useState(false); const useSafeChainsListValidation = useSelector( @@ -29,8 +33,16 @@ export function useIsOriginalNativeTokenSymbol( ); }; + const isEvm = useSelector(getMultichainIsEvm); + const providerConfig = useSelector(getMultichainCurrentNetwork); + useEffect(() => { async function getNativeTokenSymbol(networkId) { + if (!isEvm) { + setIsOriginalNativeSymbol(ticker === providerConfig?.ticker); + return; + } + try { if (!useSafeChainsListValidation) { setIsOriginalNativeSymbol(true); diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.test.js b/ui/hooks/useIsOriginalNativeTokenSymbol.test.js index c28070e47077..1b34c4b1ef81 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.test.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.test.js @@ -2,6 +2,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useSelector } from 'react-redux'; import * as fetchWithCacheModule from '../../shared/lib/fetch-with-cache'; import { useSafeChainsListValidationSelector } from '../selectors'; +import { getMultichainIsEvm } from '../selectors/multichain'; import { useIsOriginalNativeTokenSymbol } from './useIsOriginalNativeTokenSymbol'; // Adjust the import path accordingly jest.mock('react-redux', () => { @@ -14,13 +15,18 @@ jest.mock('react-redux', () => { }); const generateUseSelectorRouter = (opts) => (selector) => { + if (selector === getMultichainIsEvm) { + // If we consider testing non-EVM here, we would need to also mock those: + // - getMultichainCurrentNetwork + return true; + } if (selector === useSafeChainsListValidationSelector) { return opts; } return undefined; }; -describe('useNativeTokenFiatAmount', () => { +describe('useIsOriginalNativeTokenSymbol', () => { afterEach(() => { jest.clearAllMocks(); }); diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx index b1a0cb85b92d..a2f7cc108e4a 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx @@ -47,7 +47,7 @@ const mockAccount = createMockInternalAccount({ const mockNonEvmAccount = { ...mockAccount, id: 'b7893c59-e376-4cc0-93ad-05ddaab574a6', - addres: 'bc1qn3stuu6g37rpxk3jfxr4h4zmj68g0lwxx5eker', + address: 'bc1qn3stuu6g37rpxk3jfxr4h4zmj68g0lwxx5eker', type: BtcAccountType.P2wpkh, }; @@ -199,7 +199,7 @@ describe('useMultichainAccountTotalFiatBalance', () => { ], tokensWithBalances: [], totalFiatBalance: '100000', - totalWeiBalance: '', + totalBalance: '1.00000000', }); }); }); diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.ts b/ui/hooks/useMultichainAccountTotalFiatBalance.ts index 062014430928..9e807be41ea5 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.ts +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.ts @@ -35,7 +35,8 @@ export const useMultichainAccountTotalFiatBalance = ( isERC721: boolean; image: string; }[]; - totalWeiBalance: string; + totalWeiBalance?: string; + totalBalance?: string; loading: boolean; orderedTokenList: { iconUrl: string; symbol: string; fiatBalance: string }[]; } => { @@ -70,19 +71,14 @@ export const useMultichainAccountTotalFiatBalance = ( // BalancesController might not have updated it yet! return EMPTY_VALUES; } - const { amount } = - balances[account.id][ - MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19[ - ticker as keyof typeof MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 - ] - ]; + const { amount: balance } = balances[account.id][asset]; const totalFiatBalance = getTokenFiatAmount( 1, // coin to native conversion rate is 1:1 Number(conversionRate), // native to fiat conversion rate currentCurrency, - amount, + balance, ticker, false, false, @@ -101,7 +97,7 @@ export const useMultichainAccountTotalFiatBalance = ( return { formattedFiat, totalFiatBalance, - totalWeiBalance: '', // Not supported + totalBalance: balance, tokensWithBalances: [], // TODO: support tokens loading: false, // TODO: support tokens orderedTokenList: [nativeTokenValues], // TODO: support tokens diff --git a/ui/selectors/multichain.test.ts b/ui/selectors/multichain.test.ts index fd87387a22e3..fadbfe08a08a 100644 --- a/ui/selectors/multichain.test.ts +++ b/ui/selectors/multichain.test.ts @@ -1,4 +1,5 @@ import { Cryptocurrency } from '@metamask/assets-controllers'; +import { InternalAccount } from '@metamask/keyring-api'; import { getNativeCurrency } from '../ducks/metamask/metamask'; import { MULTICHAIN_PROVIDER_CONFIGS, @@ -9,8 +10,10 @@ import { MOCK_ACCOUNTS, MOCK_ACCOUNT_EOA, MOCK_ACCOUNT_BIP122_P2WPKH, + MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, } from '../../test/data/mock-accounts'; import { CHAIN_IDS } from '../../shared/constants/network'; +import { MultichainNativeAssets } from '../../shared/constants/multichain/assets'; import { AccountsState } from './accounts'; import { MultichainState, @@ -23,14 +26,21 @@ import { getMultichainNetwork, getMultichainNetworkProviders, getMultichainProviderConfig, + getMultichainSelectedAccountCachedBalance, getMultichainShouldShowFiat, } from './multichain'; -import { getCurrentCurrency, getCurrentNetwork, getShouldShowFiat } from '.'; +import { + getCurrentCurrency, + getCurrentNetwork, + getSelectedAccountCachedBalance, + getShouldShowFiat, +} from '.'; type TestState = MultichainState & AccountsState & { metamask: { preferences: { showFiatInTestnets: boolean }; + accountsByChainId: Record>; providerConfig: { type: string; ticker: string; chainId: string }; currentCurrency: string; currencyRates: Record; @@ -60,13 +70,26 @@ function getEvmState(): TestState { selectedAccount: MOCK_ACCOUNT_EOA.id, accounts: MOCK_ACCOUNTS, }, + accountsByChainId: { + '0x1': { + [MOCK_ACCOUNT_EOA.address]: { + balance: '3', + }, + }, + }, balances: { [MOCK_ACCOUNT_BIP122_P2WPKH.id]: { - 'bip122:000000000019d6689c085ae165831e93/slip44:0': { + [MultichainNativeAssets.BITCOIN]: { amount: '1.00000000', unit: 'BTC', }, }, + [MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET.id]: { + [MultichainNativeAssets.BITCOIN_TESTNET]: { + amount: '2.00000000', + unit: 'BTC', + }, + }, }, fiatCurrency: 'usd', cryptocurrencies: [Cryptocurrency.Btc], @@ -80,12 +103,12 @@ function getEvmState(): TestState { }; } -function getNonEvmState(): TestState { +function getNonEvmState(account = MOCK_ACCOUNT_BIP122_P2WPKH): TestState { return { metamask: { ...getEvmState().metamask, internalAccounts: { - selectedAccount: MOCK_ACCOUNT_BIP122_P2WPKH.id, + selectedAccount: account.id, accounts: MOCK_ACCOUNTS, }, }, @@ -287,12 +310,62 @@ describe('Multichain Selectors', () => { expect(getMultichainIsMainnet(state)).toBe(false); }); - it('returns current chain ID if account is non-EVM (bip122:)', () => { - const state = getNonEvmState(); + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { isMainnet: true, account: MOCK_ACCOUNT_BIP122_P2WPKH }, + { isMainnet: false, account: MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET }, + ])( + 'returns $isMainnet if non-EVM account address "$account.address" is compatible with mainnet', + ({ + isMainnet, + account, + }: { + isMainnet: boolean; + account: InternalAccount; + }) => { + const state = getNonEvmState(account); + + expect(getMultichainIsMainnet(state)).toBe(isMainnet); + }, + ); + }); - expect(getMultichainIsMainnet(state)).toBe(true); + describe('getMultichainSelectedAccountCachedBalance', () => { + it('returns cached balance if account is EVM', () => { + const state = getEvmState(); + + expect(getMultichainSelectedAccountCachedBalance(state)).toBe( + getSelectedAccountCachedBalance(state), + ); }); - // No test for testnet with non-EVM for now, as we only support mainnet network providers! + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { + network: 'mainnet', + account: MOCK_ACCOUNT_BIP122_P2WPKH, + asset: MultichainNativeAssets.BITCOIN, + }, + { + network: 'testnet', + account: MOCK_ACCOUNT_BIP122_P2WPKH_TESTNET, + asset: MultichainNativeAssets.BITCOIN_TESTNET, + }, + ])( + 'returns cached balance if account is non-EVM: $network', + ({ + account, + asset, + }: { + account: InternalAccount; + asset: MultichainNativeAssets; + }) => { + const state = getNonEvmState(account); + const balance = state.metamask.balances[account.id][asset].amount; + + state.metamask.internalAccounts.selectedAccount = account.id; + expect(getMultichainSelectedAccountCachedBalance(state)).toBe(balance); + }, + ); }); }); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1fd7c00b9f34..eba3987a21db 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -1,11 +1,7 @@ import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { ProviderConfig } from '@metamask/network-controller'; import type { RatesControllerState } from '@metamask/assets-controllers'; -import { - CaipChainId, - KnownCaipNamespace, - parseCaipChainId, -} from '@metamask/utils'; +import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; import { ChainId } from '@metamask/controller-utils'; import { MultichainProviderConfig, @@ -19,6 +15,7 @@ import { getProviderConfig, } from '../ducks/metamask/metamask'; import { BalancesControllerState } from '../../app/scripts/lib/accounts/BalancesController'; +import { MultichainNativeAssets } from '../../shared/constants/multichain/assets'; import { AccountsState } from './accounts'; import { getAllNetworks, @@ -27,6 +24,7 @@ import { getIsMainnet, getMaybeSelectedInternalAccount, getNativeCurrencyImage, + getSelectedAccountCachedBalance, getSelectedInternalAccount, getShouldShowFiat, } from '.'; @@ -91,8 +89,7 @@ export function getMultichainNetwork( const selectedAccount = account ?? getSelectedInternalAccount(state); const nonEvmNetworks = getMultichainNetworkProviders(state); const nonEvmNetwork = nonEvmNetworks.find((provider) => { - const { namespace } = parseCaipChainId(provider.chainId); - return selectedAccount.type.startsWith(namespace); + return provider.isAddressCompatible(selectedAccount.address); }); if (!nonEvmNetwork) { @@ -103,8 +100,7 @@ export function getMultichainNetwork( return { // TODO: Adapt this for other non-EVM networks - // TODO: We need to have a way of setting nicknames of other non-EVM networks - nickname: 'Bitcoin', + nickname: nonEvmNetwork.nickname, isEvmNetwork: false, // FIXME: We should use CAIP-2 chain ID here, and not only the reference part chainId: nonEvmNetwork?.chainId, @@ -209,11 +205,14 @@ export function getMultichainShouldShowFiat( true; } -export function getMultichainDefaultToken(state: MultichainState) { - const symbol = getMultichainIsEvm(state) +export function getMultichainDefaultToken( + state: MultichainState, + account?: InternalAccount, +) { + const symbol = getMultichainIsEvm(state, account) ? // We fallback to 'ETH' to keep original behavior of `getSwapsDefaultToken` getProviderConfig(state).ticker ?? 'ETH' - : getMultichainProviderConfig(state).ticker; + : getMultichainProviderConfig(state, account).ticker; return { symbol }; } @@ -223,16 +222,22 @@ export function getMultichainCurrentChainId(state: MultichainState) { return chainId; } -export function getMultichainIsMainnet(state: MultichainState) { - const chainId = getMultichainCurrentChainId(state); +export function getMultichainIsMainnet( + state: MultichainState, + account?: InternalAccount, +) { + const selectedAccount = account ?? getSelectedInternalAccount(state); + const providerConfig = getMultichainProviderConfig(state, selectedAccount); return getMultichainIsEvm(state) ? getIsMainnet(state) - : // TODO: For now we only check for bitcoin mainnet, but we will need to + : // TODO: For now we only check for bitcoin, but we will need to // update this for other non-EVM networks later! - chainId === MultichainNetworks.BITCOIN; + providerConfig.chainId === MultichainNetworks.BITCOIN; } -export function getMultichainBalances(state: MultichainState) { +export function getMultichainBalances( + state: MultichainState, +): BalancesState['metamask']['balances'] { return state.metamask.balances; } @@ -240,6 +245,26 @@ export const getMultichainCoinRates = (state: MultichainState) => { return state.metamask.rates; }; +function getBtcCachedBalance(state: MultichainState) { + const balances = getMultichainBalances(state); + const account = getSelectedInternalAccount(state); + const asset = getMultichainIsMainnet(state) + ? MultichainNativeAssets.BITCOIN + : MultichainNativeAssets.BITCOIN_TESTNET; + + return balances?.[account.id]?.[asset]?.amount; +} + +// This selector is not compatible with `useMultichainSelector` since it uses the selected +// account implicitly! +export function getMultichainSelectedAccountCachedBalance( + state: MultichainState, +) { + return getMultichainIsEvm(state) + ? getSelectedAccountCachedBalance(state) + : getBtcCachedBalance(state); +} + export function getMultichainConversionRate( state: MultichainState, account?: InternalAccount, From fde960a253cf96391ac959ff36a99cc4ff911611 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Fri, 28 Jun 2024 11:11:32 +0200 Subject: [PATCH 07/15] fix: parse tx logs on contractInteraction to refresh NFT state (#25380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** On this PR, we aim to refresh NFT ownership status or add NFTs to state if necessary by parsing transaction logs once the user submits a transaction with MM. MM controller already had the logic that calls `_updateNFTOwnership` after creating the transaction notification. That logic refreshed NFT ownerhsip when transaction type is `transferfrom`. We are adding the case when transaction type is a contract interaction and then look for specific topics. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25380?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Switch to Ethereum mainnet and go to NFTs tab (this will trigger a call to fetch your NFTs) 2. Go to opensea to buy a new NFT. 3. Click on buy and submit transaction with your MM 4. Go back to NFTs tab and you should see your new NFT without having to import it. Im using Ethereum mainnet in the videos because we support NFT detection on Ethereum mainnet. But this should also work if you are submitting transaction on Sepolia or any other test network. ## **Screenshots/Recordings** ### **Before** https://github.com/MetaMask/metamask-extension/assets/10994169/7a21efd4-c93c-4ec6-9a3d-3649b6b553df ### **After** https://github.com/MetaMask/metamask-extension/assets/10994169/b3cbaf49-f73b-4a09-96b8-b6156a5f3b98 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 180 +++++++++++++++--- shared/lib/transactions-controller-utils.js | 3 + .../tokens/nft/erc721-interaction.spec.js | 73 ++++++- test/e2e/tests/tokens/nft/send-nft.spec.js | 2 - 4 files changed, 223 insertions(+), 35 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1be7493eca72..360e30a89303 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -139,6 +139,8 @@ import { } from '@metamask/snaps-utils'; ///: END:ONLY_INCLUDE_IF +import { Interface } from '@ethersproject/abi'; +import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis'; import { isEvmAccountType } from '@metamask/keyring-api'; import { methodsRequiringNetworkSwitch, @@ -207,6 +209,10 @@ import { } from '../../shared/modules/selectors'; import { createCaipStream } from '../../shared/modules/caip-stream'; import { BaseUrl } from '../../shared/constants/urls'; +import { + TOKEN_TRANSFER_LOG_TOPIC_HASH, + TRANSFER_SINFLE_LOG_TOPIC_HASH, +} from '../../shared/lib/transactions-controller-utils'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -6344,7 +6350,7 @@ export default class MetamaskController extends EventEmitter { } await this._createTransactionNotifcation(transactionMeta); - this._updateNFTOwnership(transactionMeta); + await this._updateNFTOwnership(transactionMeta); this._trackTransactionFailure(transactionMeta); } @@ -6372,46 +6378,158 @@ export default class MetamaskController extends EventEmitter { } } - _updateNFTOwnership(transactionMeta) { + async _updateNFTOwnership(transactionMeta) { // if this is a transferFrom method generated from within the app it may be an NFT transfer transaction // in which case we will want to check and update ownership status of the transferred NFT. - const { type, txParams, chainId } = transactionMeta; + const { type, txParams, chainId, txReceipt } = transactionMeta; + const selectedAddress = + this.accountsController.getSelectedAccount().address; - if ( - type !== TransactionType.tokenMethodTransferFrom || - txParams === undefined - ) { + const { allNfts } = this.nftController.state; + const txReceiptLogs = txReceipt?.logs; + + const isContractInteractionTx = + type === TransactionType.contractInteraction && txReceiptLogs; + const isTransferFromTx = + (type === TransactionType.tokenMethodTransferFrom || + type === TransactionType.tokenMethodSafeTransferFrom) && + txParams !== undefined; + + if (!isContractInteractionTx && !isTransferFromTx) { return; } - const { data, to: contractAddress, from: userAddress } = txParams; - const transactionData = parseStandardTokenTransactionData(data); - // Sometimes the tokenId value is parsed as "_value" param. Not seeing this often any more, but still occasionally: - // i.e. call approve() on BAYC contract - https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#writeContract, and tokenId shows up as _value, - // not sure why since it doesn't match the ERC721 ABI spec we use to parse these transactions - https://github.com/MetaMask/metamask-eth-abis/blob/d0474308a288f9252597b7c93a3a8deaad19e1b2/src/abis/abiERC721.ts#L62. - const transactionDataTokenId = - getTokenIdParam(transactionData) ?? getTokenValueParam(transactionData); + if (isTransferFromTx) { + const { data, to: contractAddress, from: userAddress } = txParams; + const transactionData = parseStandardTokenTransactionData(data); + // Sometimes the tokenId value is parsed as "_value" param. Not seeing this often any more, but still occasionally: + // i.e. call approve() on BAYC contract - https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#writeContract, and tokenId shows up as _value, + // not sure why since it doesn't match the ERC721 ABI spec we use to parse these transactions - https://github.com/MetaMask/metamask-eth-abis/blob/d0474308a288f9252597b7c93a3a8deaad19e1b2/src/abis/abiERC721.ts#L62. + const transactionDataTokenId = + getTokenIdParam(transactionData) ?? getTokenValueParam(transactionData); + + // check if its a known NFT + const knownNft = allNfts?.[userAddress]?.[chainId]?.find( + ({ address, tokenId }) => + isEqualCaseInsensitive(address, contractAddress) && + tokenId === transactionDataTokenId, + ); - const { allNfts } = this.nftController.state; + // if it is we check and update ownership status. + if (knownNft) { + this.nftController.checkAndUpdateSingleNftOwnershipStatus( + knownNft, + false, + // TODO add networkClientId once it is available in the transactionMeta + // the chainId previously passed here didn't actually allow us to check for ownership on a non globally selected network + // because the check would use the provider for the globally selected network, not the chainId passed here. + { userAddress }, + ); + } + } else { + // Else if contract interaction we will parse the logs + + const allNftTransferLog = txReceiptLogs.map((txReceiptLog) => { + const isERC1155NftTransfer = + txReceiptLog.topics && + txReceiptLog.topics[0] === TRANSFER_SINFLE_LOG_TOPIC_HASH; + const isERC721NftTransfer = + txReceiptLog.topics && + txReceiptLog.topics[0] === TOKEN_TRANSFER_LOG_TOPIC_HASH; + let isTransferToSelectedAddress; + + if (isERC1155NftTransfer) { + isTransferToSelectedAddress = + txReceiptLog.topics && + txReceiptLog.topics[3] && + txReceiptLog.topics[3].match(selectedAddress?.slice(2)); + } - // check if its a known NFT - const knownNft = allNfts?.[userAddress]?.[chainId]?.find( - ({ address, tokenId }) => - isEqualCaseInsensitive(address, contractAddress) && - tokenId === transactionDataTokenId, - ); + if (isERC721NftTransfer) { + isTransferToSelectedAddress = + txReceiptLog.topics && + txReceiptLog.topics[2] && + txReceiptLog.topics[2].match(selectedAddress?.slice(2)); + } - // if it is we check and update ownership status. - if (knownNft) { - this.nftController.checkAndUpdateSingleNftOwnershipStatus( - knownNft, - false, - // TODO add networkClientId once it is available in the transactionMeta - // the chainId previously passed here didn't actually allow us to check for ownership on a non globally selected network - // because the check would use the provider for the globally selected network, not the chainId passed here. - { userAddress }, - ); + return { + isERC1155NftTransfer, + isERC721NftTransfer, + isTransferToSelectedAddress, + ...txReceiptLog, + }; + }); + if (allNftTransferLog.length !== 0) { + const allNftParsedLog = []; + allNftTransferLog.forEach((singleLog) => { + if ( + singleLog.isTransferToSelectedAddress && + (singleLog.isERC1155NftTransfer || singleLog.isERC721NftTransfer) + ) { + let iface; + if (singleLog.isERC1155NftTransfer) { + iface = new Interface(abiERC1155); + } else { + iface = new Interface(abiERC721); + } + try { + const parsedLog = iface.parseLog({ + data: singleLog.data, + topics: singleLog.topics, + }); + allNftParsedLog.push({ + contract: singleLog.address, + ...parsedLog, + }); + } catch (err) { + // ignore + } + } + }); + // Filter known nfts and new Nfts + const knownNFTs = []; + const newNFTs = []; + allNftParsedLog.forEach((single) => { + const tokenIdFromLog = getTokenIdParam(single); + const existingNft = allNfts?.[selectedAddress]?.[chainId]?.find( + ({ address, tokenId }) => { + return ( + isEqualCaseInsensitive(address, single.contract) && + tokenId === tokenIdFromLog + ); + }, + ); + if (existingNft) { + knownNFTs.push(existingNft); + } else { + newNFTs.push({ + tokenId: tokenIdFromLog, + ...single, + }); + } + }); + // For known nfts only refresh ownership + const refreshOwnershipNFts = knownNFTs.map(async (singleNft) => { + return this.nftController.checkAndUpdateSingleNftOwnershipStatus( + singleNft, + false, + // TODO add networkClientId once it is available in the transactionMeta + // the chainId previously passed here didn't actually allow us to check for ownership on a non globally selected network + // because the check would use the provider for the globally selected network, not the chainId passed here. + { selectedAddress }, + ); + }); + await Promise.allSettled(refreshOwnershipNFts); + // For new nfts, add them to state + const addNftPromises = newNFTs.map(async (singleNft) => { + return this.nftController.addNft( + singleNft.contract, + singleNft.tokenId, + ); + }); + await Promise.allSettled(addNftPromises); + } } } diff --git a/shared/lib/transactions-controller-utils.js b/shared/lib/transactions-controller-utils.js index 425ff33b6f32..073ff922af67 100644 --- a/shared/lib/transactions-controller-utils.js +++ b/shared/lib/transactions-controller-utils.js @@ -9,6 +9,9 @@ export const TOKEN_TRANSFER_LOG_TOPIC_HASH = export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract'; +export const TRANSFER_SINFLE_LOG_TOPIC_HASH = + '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62'; + export const TEN_SECONDS_IN_MILLISECONDS = 10_000; export function calcGasTotal(gasLimit = '0', gasPrice = '0') { diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index 37516fd716b8..a323077aa692 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -13,6 +13,73 @@ const FixtureBuilder = require('../../../fixture-builder'); describe('ERC721 NFTs testdapp interaction', function () { const smartContract = SMART_CONTRACTS.NFTS; + it('should add NFTs to state by parsing tx logs without having to click on watch NFT', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + title: this.test.fullTitle(), + }, + async ({ driver, _, contractRegistry }) => { + const contract = contractRegistry.getContractAddress(smartContract); + await unlockWallet(driver); + + // Open Dapp and wait for deployed contract + await openDapp(driver, contract); + await driver.findClickableElement('#deployButton'); + + // mint NFTs + await driver.fill('#mintAmountInput', '5'); + await driver.clickElement({ text: 'Mint', tag: 'button' }); + + // Notification + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const [extension] = windowHandles; + await driver.switchToWindowWithTitle( + WINDOW_TITLES.Dialog, + windowHandles, + ); + await driver.waitForSelector({ + css: '.confirm-page-container-summary__action__name', + text: 'Deposit', + }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + await driver.clickElement( + '[data-testid="account-overview__activity-tab"]', + ); + const transactionItem = await driver.waitForSelector({ + css: '[data-testid="activity-list-item-action"]', + text: 'Deposit', + }); + assert.equal(await transactionItem.isDisplayed(), true); + + // verify the mint transaction has finished + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + const nftsMintStatus = await driver.findElement({ + css: '#nftsStatus', + text: 'Mint completed', + }); + assert.equal(await nftsMintStatus.isDisplayed(), true); + + await driver.switchToWindow(extension); + + await clickNestedButton(driver, 'NFTs'); + await driver.findElement({ text: 'TestDappNFTs (5)' }); + const nftsListItemsFirstCheck = await driver.findElements( + '.nft-item__container', + ); + assert.equal(nftsListItemsFirstCheck.length, 5); + }, + ); + }); + it('should prompt users to add their NFTs to their wallet (one by one) @no-mmi', async function () { await withFixtures( { @@ -97,14 +164,16 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement({ text: 'Add NFTs', tag: 'button' }); await driver.switchToWindow(extension); await clickNestedButton(driver, 'NFTs'); - await driver.findElement({ text: 'TestDappNFTs (3)' }); + // Changed this check from 3 to 6, because after mint all nfts has been added to state, + await driver.findElement({ text: 'TestDappNFTs (6)' }); const nftsListItemsFirstCheck = await driver.findElements( '.nft-item__container', ); - assert.equal(nftsListItemsFirstCheck.length, 3); + assert.equal(nftsListItemsFirstCheck.length, 6); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); await driver.fill('#watchNFTInput', '4'); + await driver.clickElement({ text: 'Watch NFT', tag: 'button' }); await driver.fill('#watchNFTInput', '5'); await driver.clickElement({ text: 'Watch NFT', tag: 'button' }); diff --git a/test/e2e/tests/tokens/nft/send-nft.spec.js b/test/e2e/tests/tokens/nft/send-nft.spec.js index 7df8febcab56..a9b89a2abb9b 100644 --- a/test/e2e/tests/tokens/nft/send-nft.spec.js +++ b/test/e2e/tests/tokens/nft/send-nft.spec.js @@ -145,8 +145,6 @@ describe('Send NFT', function () { // Go back to NFTs tab and check the imported NFT is shown as previously owned await driver.clickElement('[data-testid="account-overview__nfts-tab"]'); - await driver.clickElement('[data-testid="refresh-list-button"]'); - const previouslyOwnedNft = await driver.findElement({ css: 'h5', text: 'Previously Owned', From 3cde03584a7c3b027cd5f97cb30f406ce62a00e1 Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:21:44 +0200 Subject: [PATCH 08/15] test: Fix flaky test: "Custom network customNetwork should add mainnet network" (#24895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses the flakiness in e2e test "Custom network customNetwork should add mainnet network." Root Cause: The flakiness happens due to occasionally hitting the API rate limit while fetching the token symbol, resulting in the warning message `"Ticker symbol verification data is currently unavailable..."`, as indicated by the failure screenshots in the CI job. However, the test is intended to verify the presence of a different warning: `"The token symbol doesn't match the network or chainID..."`. Both warnings share the same data-testid but differ in text. Consequently, when the first warning appears, the test mistakenly identifies it as the second warning, leading to test failure. Fix Implemented: The solution for this test is to refine the selector for the specific warning message we aim to verify. By making the selector more precise, we ensure that the test accurately checks for the intended warning, thereby eliminating the flakiness caused by the confusion between the two warnings. ![Screenshot 2024-05-30 at 13 55 57](https://github.com/MetaMask/metamask-extension/assets/105063779/85e9dd36-6be1-4960-8920-f2e9f5d505a0) ![Screenshot 2024-05-30 at 11 57 16](https://github.com/MetaMask/metamask-extension/assets/105063779/5e47af8d-4c0f-4b31-a05e-0b9f9da04106) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24895?quickstart=1) ## **Related issues** Fixes: #24634 ## **Manual testing steps** 1. Run the test several times yarn test:e2e:single test/e2e/tests/network/add-custom-network.spec.js --browser=firefox --leave-running --retryUntilFailure --retries=10 2. Check ci jobs ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- test/e2e/tests/network/add-custom-network.spec.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index 0cd18c13a9bf..fd133fa47e47 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -88,6 +88,14 @@ const selectors = { }, suggestedTicker: '[data-testid="network-form-ticker-suggestion"]', tickerWarning: '[data-testid="network-form-ticker-warning"]', + suggestedTickerForXDAI: { + css: '[data-testid="network-form-ticker-suggestion"]', + text: 'Suggested ticker symbol: XDAI', + }, + tickerWarningTokenSymbol: { + css: '[data-testid="network-form-ticker-warning"]', + text: "This token symbol doesn't match the network name or chain ID entered.", + }, tickerButton: { text: 'PETH', tag: 'button' }, networkAdded: { text: 'Network added successfully!', tag: 'h4' }, @@ -735,11 +743,11 @@ describe('Custom network', function () { await driver.fill(selectors.explorerInputField, 'https://test.com'); const suggestedTicker = await driver.isElementPresent( - selectors.suggestedTicker, + selectors.suggestedTickerForXDAI, ); const tickerWarning = await driver.isElementPresent( - selectors.tickerWarning, + selectors.tickerWarningTokenSymbol, ); assert.equal(suggestedTicker, false); From 1661b212645a0d159a90a5b0d75e54ecc73090aa Mon Sep 17 00:00:00 2001 From: Javier Briones <1674192+jvbriones@users.noreply.github.com> Date: Fri, 28 Jun 2024 15:55:33 +0200 Subject: [PATCH 09/15] ci: add SonarCloud scan job (#25421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add a SonarCloud scan job ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .github/workflows/sonar.yml | 22 ++++++---------------- sonar-project.properties | 26 +++++++++++--------------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 61d549728d99..f5e1a0552dd1 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -1,18 +1,11 @@ name: Sonar on: - workflow_call: - secrets: - SONAR_TOKEN: - required: true -# pull_request: -# branches: -# - develop -# types: -# - opened -# - reopened -# - synchronize -# - labeled -# - unlabeled + push: + branches: + - develop + pull_request: + branches: + - develop jobs: sonarcloud: @@ -25,8 +18,5 @@ jobs: - name: SonarCloud Scan # This is SonarSource/sonarcloud-github-action@v2.0.0 uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 - with: - args: > - -Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/sonar-project.properties b/sonar-project.properties index de14094b965e..0455fa9634e2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,18 +1,14 @@ -sonar.projectKey=metamask-extension -sonar.organization=consensys +sonar.projectKey=metamask-extension-private +sonar.organization=metamask -# This is the name and version displayed in the SonarCloud UI. -sonar.projectName=MetaMask Extension -#sonar.projectVersion=1.0 +# Source +sonar.sources=app,development,offscreen,shared,types,ui +sonar.exclusions=**/*.test.**,**/*.spec.**,app/images -# Root for sonar analysis. -sonar.sources=app/ +# Tests +sonar.tests=app,test,development,offscreen,shared,types,ui +sonar.test.inclusions=**/*.test.**,**/*.spec.** +sonar.javascript.lcov.reportPaths=tests/coverage/lcov.info -# Excluded project files from analysis. -#sonar.exclusions= - -# Inclusions for test files. -sonar.test.inclusions=**.test.** - -# Encoding of the source code. Default is default system encoding -sonar.sourceEncoding=UTF-8 +# Fail CI job if quality gate failures +sonar.qualitygate.wait=false \ No newline at end of file From fdc898e1670d1c973a165a1a09743f9a227d668a Mon Sep 17 00:00:00 2001 From: David Walsh Date: Fri, 28 Jun 2024 09:54:36 -0500 Subject: [PATCH 10/15] test: UX: Multichain: Add test for mutiple dapp confirmation order (#25536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR ensures no dapp-specified transactions get dropped when triggering transactions on dapps, switching to a dapp on another chain, and triggering another transaction. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25536?quickstart=1) ## **Related issues** Supercedes: https://github.com/MetaMask/metamask-extension/pull/25459 Fixes: ## **Manual testing steps** 1. Open `Tab 1`, connect to `Uniswap` on `Ethereum Mainnet` 2. Open `Tab 2`, connect to `PancakeSwap` on `BNB Chain` 3. Open `Tab 3`, connect to `Test Dapp` on `Sepolia` 4. Initiate a swap on `Tab 1` and `Tab 2` *BUT DO NOT CONFIRM IT, JUST MOVE ON TO THE NEXT TAB* 5. Initiate a send on `Tab 3` *BUT DO NOT CONFIRM IT* 6. On the confirmation screen, you should still see the first confirmation from `Tab 1` (`Uniswap`) on `Ethereum Mainnet`; confirm or reject it. See the confirmation window close 7. A new confirmation popup should come up with the `PancakeSwap`/ `Tab 2` confirmation on `BNB` chain; confirm or reject it. See the confirmation window close 8. See one last confirmation screen pop up for the `Tab 3` / `Test Dapp` send on `Sepolia`. Confirm or reject it. ## **Screenshots/Recordings** ### **Before** ### **After** Video of equivalent, manual test of this E2E https://github.com/MetaMask/metamask-extension/assets/46655/cc4578f9-602c-4c2e-835d-79e4ca8ed762 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/helpers.js | 2 + test/e2e/tests/request-queuing/ui.spec.js | 270 +++++++++++++++++++--- 2 files changed, 238 insertions(+), 34 deletions(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 41951f973aa9..ba32c94cabec 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -664,6 +664,7 @@ const closeSRPReveal = async (driver) => { const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; const DAPP_ONE_URL = 'http://127.0.0.1:8081'; +const DAPP_TWO_URL = 'http://127.0.0.1:8082'; const openDapp = async (driver, contract = null, dappURL = DAPP_URL) => { return contract @@ -1121,6 +1122,7 @@ module.exports = { DAPP_HOST_ADDRESS, DAPP_URL, DAPP_ONE_URL, + DAPP_TWO_URL, TEST_SEED_PHRASE, TEST_SEED_PHRASE_TWO, PRIVATE_KEY, diff --git a/test/e2e/tests/request-queuing/ui.spec.js b/test/e2e/tests/request-queuing/ui.spec.js index e85d87226e84..45429ad32263 100644 --- a/test/e2e/tests/request-queuing/ui.spec.js +++ b/test/e2e/tests/request-queuing/ui.spec.js @@ -1,4 +1,5 @@ const { strict: assert } = require('assert'); +const { Browser } = require('selenium-webdriver'); const FixtureBuilder = require('../../fixture-builder'); const { withFixtures, @@ -11,21 +12,33 @@ const { defaultGanacheOptions, switchToNotificationWindow, veryLargeDelayMs, + DAPP_TWO_URL, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); -async function openDappAndSwitchChain(driver, dappUrl, chainId) { - const notificationWindowIndex = chainId ? 4 : 3; +// Window handle adjustments will need to be made for Non-MV3 Firefox +// due to OffscreenDocument. Additionally Firefox continually bombs +// with a "NoSuchWindowError: Browsing context has been discarded" whenever +// we try to open a third dapp, so this test run in Firefox will +// validate two dapps instead of 3 +const IS_FIREFOX = process.env.SELENIUM_BROWSER === Browser.FIREFOX; +async function openDappAndSwitchChain( + driver, + dappUrl, + chainId, + notificationWindowIndex = 3, +) { // Open the dapp await openDapp(driver, undefined, dappUrl); - await driver.delay(regularDelayMs); // Connect to the dapp await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); await driver.delay(regularDelayMs); + await switchToNotificationWindow(driver, notificationWindowIndex); + await driver.clickElement({ text: 'Next', tag: 'button', @@ -62,39 +75,99 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { } } -async function selectDappClickSendGetNetwork(driver, dappUrl) { +async function selectDappClickSend(driver, dappUrl) { await driver.switchToWindowWithUrl(dappUrl); - // Windows: MetaMask, TestDapp1, TestDapp2 - const expectedWindowHandles = 3; - await driver.waitUntilXWindowHandles(expectedWindowHandles); - const currentWindowHandles = await driver.getAllWindowHandles(); await driver.clickElement('#sendButton'); +} - // Under mv3, we don't need to add to the current number of window handles - // because the offscreen document returned by getAllWindowHandles provides - // an extra window handle - const newWindowHandles = await driver.waitUntilXWindowHandles( - process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined - ? currentWindowHandles.length - : currentWindowHandles.length + 1, - ); - const [newNotificationWindowHandle] = newWindowHandles.filter( - (h) => !currentWindowHandles.includes(h), - ); - await driver.switchToWindow(newNotificationWindowHandle); +async function switchToNotificationPopoverValidateDetails( + driver, + expectedDetails, +) { + // Switches to the MetaMask Dialog window for confirmation + const windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog, windowHandles); + // Get UI details const networkPill = await driver.findElement( '[data-testid="network-display"]', ); const networkText = await networkPill.getText(); - await driver.clickElement({ css: 'button', text: 'Reject' }); - return networkText; + const originElement = await driver.findElement( + '.confirm-page-container-summary__origin bdi', + ); + const originText = await originElement.getText(); + + // Get state details + const notificationWindowState = await driver.executeScript(() => + window.stateHooks?.getCleanAppState?.(), + ); + const { chainId } = notificationWindowState.metamask.providerConfig; + + // Ensure accuracy + validateConfirmationDetails( + { networkText, originText, chainId }, + expectedDetails, + ); +} + +async function rejectTransaction(driver) { + await driver.clickElement({ tag: 'button', text: 'Reject' }); +} + +async function confirmTransaction(driver) { + await driver.clickElement({ tag: 'button', text: 'Confirm' }); +} + +function validateConfirmationDetails( + { chainId, networkText, originText }, + expected, +) { + assert.equal(chainId, expected.chainId); + assert.equal(networkText, expected.networkText); + assert.equal(originText, expected.originText); +} + +async function switchToNetworkByName(driver, networkName) { + await driver.clickElement('[data-testid="network-display"]'); + await driver.clickElement(`[data-testid="${networkName}"]`); +} + +async function validateBalanceAndActivity( + driver, + expectedBalance, + expectedActivityEntries = 1, +) { + // Ensure the balance changed if the the transaction was confirmed + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"] .currency-display-component__text', + text: expectedBalance, + }); + + // Ensure there's an activity entry of "Send" and "Confirmed" + if (expectedActivityEntries) { + await driver.clickElement('[data-testid="account-overview__activity-tab"]'); + assert.equal( + ( + await driver.findElements({ + css: '[data-testid="activity-list-item-action"]', + text: 'Send', + }) + ).length, + expectedActivityEntries, + ); + assert.equal( + (await driver.findElements('.transaction-status-label--confirmed')) + .length, + expectedActivityEntries, + ); + } } describe('Request-queue UI changes', function () { it('UI should show network specific to domain @no-mmi', async function () { const port = 8546; - const chainId = 1338; + const chainId = 1338; // 0x53a await withFixtures( { dapp: true, @@ -126,7 +199,7 @@ describe('Request-queue UI changes', function () { await openDappAndSwitchChain(driver, DAPP_URL); // Open the second dapp and switch chains - await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); + await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a', 4); // Go to wallet fullscreen, ensure that the global network changed to Ethereum Mainnet await driver.switchToWindowWithTitle( @@ -134,22 +207,151 @@ describe('Request-queue UI changes', function () { ); await driver.findElement({ css: '[data-testid="network-display"]', - text: 'Ethereum Mainnet', + text: 'Localhost 8546', }); // Go to the first dapp, ensure it uses localhost - const dappOneNetworkPillText = await selectDappClickSendGetNetwork( - driver, - DAPP_URL, - ); - assert.equal(dappOneNetworkPillText, 'Localhost 8545'); + await selectDappClickSend(driver, DAPP_URL); + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x539', + networkText: 'Localhost 8545', + originText: DAPP_URL, + }); + await rejectTransaction(driver); // Go to the second dapp, ensure it uses Ethereum Mainnet - const dappTwoNetworkPillText = await selectDappClickSendGetNetwork( - driver, - DAPP_ONE_URL, + await selectDappClickSend(driver, DAPP_ONE_URL); + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x53a', + networkText: 'Localhost 8546', + originText: DAPP_ONE_URL, + }); + await rejectTransaction(driver); + }, + ); + }); + + it('handles three confirmations on three confirmations concurrently @no-mmi', async function () { + const port = 8546; + const chainId = 1338; // 0x53a + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + // Ganache for network 1 + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + // Ganache for network 3 + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + dappOptions: { numberOfDapps: 3 }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Navigate to extension home screen + await driver.navigate(PAGES.HOME); + + // Open the first dapp + await openDappAndSwitchChain(driver, DAPP_URL); + + // Open the second dapp and switch chains + await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a', 4); + + if (!IS_FIREFOX) { + // Open the third dapp and switch chains + await openDappAndSwitchChain(driver, DAPP_TWO_URL, '0x3e8', 5); + } + + // Trigger a send confirmation on the first dapp, do not confirm or reject + await selectDappClickSend(driver, DAPP_URL); + + // Trigger a send confirmation on the second dapp, do not confirm or reject + await selectDappClickSend(driver, DAPP_ONE_URL); + + if (!IS_FIREFOX) { + // Trigger a send confirmation on the third dapp, do not confirm or reject + await selectDappClickSend(driver, DAPP_TWO_URL); + } + + // Switch to the Notification window, ensure first transaction still showing + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x539', + networkText: 'Localhost 8545', + originText: DAPP_URL, + }); + + // Confirm transaction, wait for first confirmation window to close, second to display + await confirmTransaction(driver); + await driver.delay(veryLargeDelayMs); + + // Switch to the new Notification window, ensure second transaction showing + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x53a', + networkText: 'Localhost 8546', + originText: DAPP_ONE_URL, + }); + + // Reject this transaction, wait for second confirmation window to close, third to display + await rejectTransaction(driver); + await driver.delay(veryLargeDelayMs); + + if (!IS_FIREFOX) { + // Switch to the new Notification window, ensure third transaction showing + await switchToNotificationPopoverValidateDetails(driver, { + chainId: '0x3e8', + networkText: 'Localhost 7777', + originText: DAPP_TWO_URL, + }); + + // Confirm transaction + await confirmTransaction(driver); + } + + // With first and last confirmations confirmed, and second rejected, + // Ensure only first and last network balances were affected + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Wait for transaction to be completed on final confirmation + await driver.delay(veryLargeDelayMs); + + if (!IS_FIREFOX) { + // Start on the last joined network, whose send transaction was just confirmed + await validateBalanceAndActivity(driver, '24.9998'); + } + + // Switch to second network, ensure full balance + await switchToNetworkByName(driver, 'Localhost 8546'); + await validateBalanceAndActivity(driver, '25', 0); + + // Turn on test networks in Networks menu so Localhost 8545 is available + await driver.clickElement('[data-testid="network-display"]'); + await driver.clickElement('.mm-modal-content__dialog .toggle-button'); + await driver.clickElement( + '.mm-modal-content__dialog button[aria-label="Close"]', ); - assert.equal(dappTwoNetworkPillText, 'Ethereum Mainnet'); + + // Switch to first network, whose send transaction was just confirmed + await switchToNetworkByName(driver, 'Localhost 8545'); + await validateBalanceAndActivity(driver, '24.9998'); }, ); }); From 1f74b08fc18b6462b9624e7d33a2da01191c5e9b Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Fri, 28 Jun 2024 08:18:35 -0700 Subject: [PATCH 11/15] feat: adding / deleting additional RPC URLs (#25452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Initial UI for adding and deleting multiple RPC URLs. The add and delete buttons don't do anything yet. Just UI until the network controller gets upgraded. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25452?quickstart=1) ## **Related issues** ## **Manual testing steps** 1. Build with ENABLE_NETWORK_UI_REDESIGN=1 yarn start 2. Open networks, right click edit one 3. Click RPC dropdown 4. Add and delete RPC endpoints ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/MetaMask/metamask-extension/assets/3500406/42047230-b258-4c1b-a37e-fb3c0f300913 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: David Walsh Co-authored-by: salimtb --- app/_locales/en/messages.json | 15 ++++ .../confirm-delete-rpc-url-modal.tsx | 72 +++++++++++++++++ ui/components/app/modals/modal.js | 14 ++++ .../add-rpc-url-modal/add-rpc-url-modal.tsx | 43 ++++++++++ .../network-list-menu/network-list-menu.js | 80 +++++++++++++------ ui/ducks/app/app.ts | 13 ++- ui/pages/home/home.component.js | 9 ++- ui/pages/home/home.container.js | 2 +- .../add-network-modal/index.js | 3 + ui/pages/routes/routes.component.js | 9 ++- ui/pages/routes/routes.container.js | 2 + ui/pages/settings/networks-tab/index.scss | 2 + .../networks-form/networks-form.js | 16 +++- .../networks-form/rpc-url-editor.tsx | 47 ++++++++--- ui/selectors/selectors.js | 4 + ui/store/actions.test.js | 2 + ui/store/actions.ts | 20 ++--- 17 files changed, 294 insertions(+), 59 deletions(-) create mode 100644 ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx create mode 100644 ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f50a5fa84362..25f3c5436158 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -311,6 +311,9 @@ "message": "Can’t find a token? You can manually add any token by pasting its address. Token contract addresses can be found on $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Add URL" + }, "addingCustomNetwork": { "message": "Adding Network" }, @@ -320,6 +323,9 @@ "additionalNetworks": { "message": "Additional networks" }, + "additionalRpcUrl": { + "message": "Additional RPC URL" + }, "address": { "message": "Address" }, @@ -972,6 +978,9 @@ "confirmConnectionTitle": { "message": "Confirm connection to $1" }, + "confirmDeletion": { + "message": "Confirm deletion" + }, "confirmFieldPaymaster": { "message": "Fee paid by" }, @@ -984,6 +993,9 @@ "confirmRecoveryPhrase": { "message": "Confirm Secret Recovery Phrase" }, + "confirmRpcUrlDeletionMessage": { + "message": "Are you sure you want to delete the RPC URL? Your information will not be saved for this network." + }, "confirmTitleDescContractInteractionTransaction": { "message": "Only confirm this transaction if you fully understand the content and trust the requesting site." }, @@ -1463,6 +1475,9 @@ "message": "Delete $1 network?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Delete RPC URL" + }, "deposit": { "message": "Deposit" }, diff --git a/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx b/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx new file mode 100644 index 000000000000..3eb9a9323048 --- /dev/null +++ b/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + BlockSize, + Display, +} from '../../../../helpers/constants/design-system'; +import { + Box, + ButtonPrimary, + ButtonPrimarySize, + ButtonSecondary, + ButtonSecondarySize, + Modal, + ModalBody, + ModalContent, + ModalHeader, + ModalOverlay, +} from '../../../component-library'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + hideModal, + setEditedNetwork, + toggleNetworkMenu, +} from '../../../../store/actions'; + +const ConfirmDeleteRpcUrlModal = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + return ( + { + dispatch(setEditedNetwork()); + dispatch(hideModal()); + }} + > + + + {t('confirmDeletion')} + + {t('confirmRpcUrlDeletionMessage')} + + { + dispatch(hideModal()); + dispatch(toggleNetworkMenu()); + }} + > + {t('back')} + + { + console.log('TODO: Delete RPc URL'); + }} + > + {t('deleteRpcUrl')} + + + + + + ); +}; + +export default ConfirmDeleteRpcUrlModal; diff --git a/ui/components/app/modals/modal.js b/ui/components/app/modals/modal.js index f3eb1a950a40..9d546e5de74d 100644 --- a/ui/components/app/modals/modal.js +++ b/ui/components/app/modals/modal.js @@ -38,6 +38,7 @@ import TransactionAlreadyConfirmed from './transaction-already-confirmed'; // Metamask Notifications import ConfirmTurnOffProfileSyncing from './confirm-turn-off-profile-syncing'; import TurnOnMetamaskNotifications from './turn-on-metamask-notifications/turn-on-metamask-notifications'; +import ConfirmDeleteRpcUrlModal from './confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal'; const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -230,6 +231,19 @@ const MODALS = { }, }, + CONFIRM_DELETE_RPC_URL: { + contents: , + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + EDIT_APPROVAL_PERMISSION: { contents: , mobileModalStyle: { diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx new file mode 100644 index 000000000000..8e4269928bc8 --- /dev/null +++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { + Box, + ButtonPrimary, + ButtonPrimarySize, + FormTextField, +} from '../../../component-library'; +import { + BlockSize, + Display, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +const AddRpcUrlModal = () => { + const t = useI18nContext(); + + return ( + + + + + {t('addUrl')} + + + ); +}; + +export default AddRpcUrlModal; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index c481b163f8a0..3a1f279103d4 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { useDispatch, useSelector } from 'react-redux'; @@ -15,6 +15,7 @@ import { toggleNetworkMenu, updateNetworksList, setNetworkClientIdForDomain, + setEditedNetwork, } from '../../../store/actions'; import { FEATURED_RPCS, @@ -32,6 +33,7 @@ import { getOriginOfCurrentTab, getUseRequestQueue, getNetworkConfigurations, + getEditedNetwork, } from '../../../selectors'; import ToggleButton from '../../ui/toggle-button'; import { @@ -70,6 +72,7 @@ import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../helpers/utils/f import AddNetworkModal from '../../../pages/onboarding-flow/add-network-modal'; import PopularNetworkList from './popular-network-list/popular-network-list'; import NetworkListSearch from './network-list-search/network-list-search'; +import AddRpcUrlModal from './add-rpc-url-modal/add-rpc-url-modal'; const ACTION_MODES = { // Displays the search box and network list @@ -78,14 +81,13 @@ const ACTION_MODES = { ADD: 'add', // Displays the Edit form EDIT: 'edit', + // Displays the page for adding an additional RPC URL + ADD_RPC: 'add_rpc', }; export const NetworkListMenu = ({ onClose }) => { const t = useI18nContext(); - const [actionMode, setActionMode] = useState(ACTION_MODES.LIST); - const [modalTitle, setModalTitle] = useState(t('networkMenuHeading')); - const [networkToEdit, setNetworkToEdit] = useState(null); const nonTestNetworks = useSelector(getNonTestNetworks); const testNetworks = useSelector(getTestNetworks); const showTestNetworks = useSelector(getShowTestNetworks); @@ -114,6 +116,19 @@ export const NetworkListMenu = ({ onClose }) => { const orderedNetworksList = useSelector(getOrderedNetworksList); + const editedNetwork = useSelector(getEditedNetwork); + + const [actionMode, setActionMode] = useState( + editedNetwork ? ACTION_MODES.EDIT : ACTION_MODES.LIST, + ); + + const networkToEdit = useMemo(() => { + const network = [...nonTestNetworks, ...testNetworks].find( + (n) => n.id === editedNetwork?.networkConfigurationId, + ); + return network ? { ...network, label: network.nickname } : undefined; + }, [editedNetwork, nonTestNetworks, testNetworks]); + const networkConfigurationChainIds = Object.values(networkConfigurations).map( (net) => net.chainId, ); @@ -259,12 +274,12 @@ export const NetworkListMenu = ({ onClose }) => { const getOnEditCallback = (network) => { return () => { - const networkToUse = { - ...network, - label: network.nickname, - }; - setModalTitle(network.nickname); - setNetworkToEdit(networkToUse); + dispatch( + setEditedNetwork({ + networkConfigurationId: network.id, + nickname: network.nickname, + }), + ); setActionMode(ACTION_MODES.EDIT); }; }; @@ -518,7 +533,6 @@ export const NetworkListMenu = ({ onClose }) => { category: MetaMetricsEventCategory.Network, }); setActionMode(ACTION_MODES.ADD); - setModalTitle(t('addCustomNetwork')); }} > {t('addNetwork')} @@ -528,20 +542,38 @@ export const NetworkListMenu = ({ onClose }) => { ); } else if (actionMode === ACTION_MODES.ADD) { return ; + } else if (actionMode === ACTION_MODES.EDIT) { + return ( + setActionMode(ACTION_MODES.ADD_RPC)} + /> + ); + } else if (actionMode === ACTION_MODES.ADD_RPC) { + return ; } - return ( - - ); + return null; // Unreachable, but satisfies linter }; - const headerAdditionalProps = - actionMode === ACTION_MODES.LIST - ? {} - : { onBack: () => setActionMode(ACTION_MODES.LIST) }; + // Modal back button + let onBack; + if (actionMode === ACTION_MODES.EDIT || actionMode === ACTION_MODES.ADD) { + onBack = () => setActionMode(ACTION_MODES.LIST); + } else if (actionMode === ACTION_MODES.ADD_RPC) { + onBack = () => setActionMode(ACTION_MODES.EDIT); + } + + // Modal title + let title; + if (actionMode === ACTION_MODES.LIST) { + title = t('networkMenuHeading'); + } else if (actionMode === ACTION_MODES.ADD) { + title = t('addCustomNetwork'); + } else { + title = editedNetwork.nickname; + } return ( @@ -560,9 +592,9 @@ export const NetworkListMenu = ({ onClose }) => { paddingRight={4} paddingBottom={6} onClose={onClose} - {...headerAdditionalProps} + onBack={onBack} > - {modalTitle} + {title} {renderListNetworks()} diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index dc5fa3ba64f6..a16508c9a45a 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -82,7 +82,13 @@ type AppState = { newNftAddedMessage: string; removeNftMessage: string; newNetworkAddedName: string; - editedNetwork: string; + editedNetwork: + | { + networkConfigurationId: string; + nickname: string; + editCompleted: boolean; + } + | undefined; newNetworkAddedConfigurationId: string; selectedNetworkConfigurationId: string; sendInputCurrencySwitched: boolean; @@ -163,7 +169,7 @@ const initialState: AppState = { newNftAddedMessage: '', removeNftMessage: '', newNetworkAddedName: '', - editedNetwork: '', + editedNetwork: undefined, newNetworkAddedConfigurationId: '', selectedNetworkConfigurationId: '', sendInputCurrencySwitched: false, @@ -489,10 +495,9 @@ export default function reduceApp( }; } case actionConstants.SET_EDIT_NETWORK: { - const { nickname } = action.payload; return { ...appState, - editedNetwork: nickname, + editedNetwork: action.payload, }; } case actionConstants.SET_NEW_TOKENS_IMPORTED: diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index cb177340501d..33ca2b67a181 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -80,6 +80,7 @@ import { ///: END:ONLY_INCLUDE_IF } from '../../../shared/lib/ui-utils'; import { AccountOverview } from '../../components/multichain/account-overview'; +import { setEditedNetwork } from '../../store/actions'; ///: BEGIN:ONLY_INCLUDE_IF(build-beta) import BetaHomeFooter from './beta/beta-home-footer.component'; ///: END:ONLY_INCLUDE_IF @@ -185,7 +186,7 @@ export default class Home extends PureComponent { showOutdatedBrowserWarning: PropTypes.bool.isRequired, setOutdatedBrowserWarningLastShown: PropTypes.func.isRequired, newNetworkAddedName: PropTypes.string, - editedNetwork: PropTypes.string, + editedNetwork: PropTypes.object, // This prop is used in the `shouldCloseNotificationPopup` function // eslint-disable-next-line react/no-unused-prop-types isSigningQRHardwareTransaction: PropTypes.bool.isRequired, @@ -499,7 +500,7 @@ export default class Home extends PureComponent { setRemoveNftMessage(''); setNewTokensImported(''); // Added this so we dnt see the notif if user does not close it setNewTokensImportedError(''); - clearEditedNetwork({}); + setEditedNetwork(); }; const autoHideDelay = 5 * SECOND; @@ -606,7 +607,7 @@ export default class Home extends PureComponent { } /> ) : null} - {editedNetwork ? ( + {editedNetwork?.editCompleted ? ( - {t('newNetworkEdited', [editedNetwork])} + {t('newNetworkEdited', [editedNetwork.nickname])} { dispatch(setNewNetworkAdded({})); }, clearEditedNetwork: () => { - dispatch(setEditedNetwork({})); + dispatch(setEditedNetwork()); }, setActiveNetwork: (networkConfigurationId) => { dispatch(setActiveNetwork(networkConfigurationId)); diff --git a/ui/pages/onboarding-flow/add-network-modal/index.js b/ui/pages/onboarding-flow/add-network-modal/index.js index c031739b68b6..b35785e4f2ac 100644 --- a/ui/pages/onboarding-flow/add-network-modal/index.js +++ b/ui/pages/onboarding-flow/add-network-modal/index.js @@ -19,6 +19,7 @@ export default function AddNetworkModal({ isNewNetworkFlow = false, addNewNetwork = true, networkToEdit = null, + onRpcUrlAdd, }) { const dispatch = useDispatch(); const t = useI18nContext(); @@ -50,6 +51,7 @@ export default function AddNetworkModal({ networksToRender={[]} cancelCallback={closeCallback} submitCallback={closeCallback} + onRpcUrlAdd={onRpcUrlAdd} isNewNetworkFlow={isNewNetworkFlow} {...additionalProps} /> @@ -62,6 +64,7 @@ AddNetworkModal.propTypes = { isNewNetworkFlow: PropTypes.bool, addNewNetwork: PropTypes.bool, networkToEdit: PropTypes.object, + onRpcUrlAdd: PropTypes.func, }; AddNetworkModal.defaultProps = { diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index bec55f91a15e..9fe51d046eec 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -209,6 +209,7 @@ export default class Routes extends Component { newPrivacyPolicyToastShownDate: PropTypes.number, setSurveyLinkLastClickedOrClosed: PropTypes.func.isRequired, setNewPrivacyPolicyToastShownDate: PropTypes.func.isRequired, + clearEditedNetwork: PropTypes.func.isRequired, setNewPrivacyPolicyToastClickedOrClosed: PropTypes.func.isRequired, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal: PropTypes.bool.isRequired, @@ -804,6 +805,7 @@ export default class Routes extends Component { switchedNetworkDetails, clearSwitchedNetworkDetails, networkMenuRedesign, + clearEditedNetwork, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal, hideShowKeyringSnapRemovalResultModal, @@ -886,7 +888,12 @@ export default class Routes extends Component { toggleAccountMenu()} /> ) : null} {isNetworkMenuOpen ? ( - toggleNetworkMenu()} /> + { + toggleNetworkMenu(); + clearEditedNetwork(); + }} + /> ) : null} {networkMenuRedesign ? : null} {accountDetailsAddress ? ( diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index d4334e00b4de..da6d62636d5f 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -52,6 +52,7 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hideKeyringRemovalResultModal, ///: END:ONLY_INCLUDE_IF + setEditedNetwork, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -176,6 +177,7 @@ function mapDispatchToProps(dispatch) { dispatch(setNewPrivacyPolicyToastClickedOrClosed()), setNewPrivacyPolicyToastShownDate: (date) => dispatch(setNewPrivacyPolicyToastShownDate(date)), + clearEditedNetwork: () => dispatch(setEditedNetwork()), ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hideShowKeyringSnapRemovalResultModal: () => dispatch(hideKeyringRemovalResultModal()), diff --git a/ui/pages/settings/networks-tab/index.scss b/ui/pages/settings/networks-tab/index.scss index 99a4840aead0..86e792d988c4 100644 --- a/ui/pages/settings/networks-tab/index.scss +++ b/ui/pages/settings/networks-tab/index.scss @@ -7,10 +7,12 @@ &__rpc-dropdown { cursor: pointer; + word-break: break-all; } &__rpc-item { position: relative; + word-break: break-all; } &__rpc-item:hover { diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js index 298a063f98b6..c5bdaeeb1fc5 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -120,6 +120,7 @@ const NetworksForm = ({ selectedNetwork, cancelCallback, submitCallback, + onRpcUrlAdd, }) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -795,7 +796,13 @@ const NetworksForm = ({ }, }); if (networkMenuRedesign) { - dispatch(setEditedNetwork({ nickname: networkName })); + dispatch( + setEditedNetwork({ + networkConfigurationId, + nickname: networkName, + editCompleted: true, + }), + ); } } @@ -925,8 +932,12 @@ const NetworksForm = ({ ))} ) : null} + {networkMenuRedesign ? ( - + ) : ( { +import { showModal, toggleNetworkMenu } from '../../../../store/actions'; + +export const RpcUrlEditor = ({ + currentRpcUrl, + onRpcUrlAdd, +}: { + currentRpcUrl: string; + onRpcUrlAdd: () => void; +}) => { // TODO: real endpoints const dummyRpcUrls = [ currentRpcUrl, - 'https://dummy.mainnet.public.blastapi.io', - 'https://dummy.io/v3/blockchain/node/dummy', + 'https://mainnet.public.blastapi.io', + 'https://infura.foo.bar.baz/123456789', ]; const t = useI18nContext(); + const dispatch = useDispatch(); const rpcDropdown = useRef(null); - const [isOpen, setIsOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [currentRpcEndpoint, setCurrentRpcEndpoint] = useState(currentRpcUrl); return ( @@ -48,7 +58,7 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { {t('defaultRpcUrl')} setIsOpen(!isOpen)} + onClick={() => setIsDropdownOpen(!isDropdownOpen)} className="networks-tab__rpc-dropdown" display={Display.Flex} justifyContent={JustifyContent.spaceBetween} @@ -60,7 +70,7 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { > {currentRpcEndpoint} @@ -69,19 +79,24 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { paddingTop={2} paddingBottom={2} paddingLeft={0} + matchWidth={true} paddingRight={0} className="networks-tab__rpc-popover" referenceElement={rpcDropdown.current} position={PopoverPosition.Bottom} - isOpen={isOpen} + isOpen={isDropdownOpen} > {dummyRpcUrls.map((rpcEndpoint) => ( setCurrentRpcEndpoint(rpcEndpoint)} + onClick={() => { + setCurrentRpcEndpoint(rpcEndpoint); + setIsDropdownOpen(false); + }} className={classnames('networks-tab__rpc-item', { 'networks-tab__rpc-item--selected': rpcEndpoint === currentRpcEndpoint, @@ -103,19 +118,25 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { {rpcEndpoint} alert('TODO: delete confirmation modal')} + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + dispatch(toggleNetworkMenu()); + dispatch( + showModal({ + name: 'CONFIRM_DELETE_RPC_URL', + }), + ); + }} /> ))} alert('TODO: add RPC modal')} + onClick={onRpcUrlAdd} padding={4} display={Display.Flex} alignItems={AlignItems.center} diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index cd049873dfa4..faec9242d616 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2026,6 +2026,10 @@ export function getNewNetworkAdded(state) { return state.appState.newNetworkAddedName; } +/** + * @param state + * @returns {{ networkConfigurationId: string; nickname: string; editCompleted: boolean} | undefined} + */ export function getEditedNetwork(state) { return state.appState.editedNetwork; } diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 813153d33ca3..aa161ba781a1 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1395,6 +1395,8 @@ describe('Actions', () => { const newNetworkAddedDetails = { nickname: 'test-chain', + networkConfigurationId: 'testNetworkConfigurationId', + editCompleted: true, }; store.dispatch(actions.setEditedNetwork(newNetworkAddedDetails)); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 4a75d4828212..186e59ccb8df 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4139,16 +4139,16 @@ export function setNewNetworkAdded({ }; } -export function setEditedNetwork({ - nickname, -}: { - networkConfigurationId: string; - nickname: string; -}): PayloadAction { - return { - type: actionConstants.SET_EDIT_NETWORK, - payload: { nickname }, - }; +export function setEditedNetwork( + payload: + | { + networkConfigurationId: string; + nickname: string; + editCompleted: boolean; + } + | undefined = undefined, +): PayloadAction { + return { type: actionConstants.SET_EDIT_NETWORK, payload }; } export function setNewNftAddedMessage( From 4a9c48084e7cd55a7dbd7258ec28fea8919a8a1e Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 28 Jun 2024 12:16:16 -0500 Subject: [PATCH 12/15] fix: add eth_signTypedData and eth_signTypedData_v3 to `methodsRequiringNetworkSwitch` (#25562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Though signature requests like `personal_sign`, `eth_signTypedData`, `eth_signTypedData_v3` and `eth_signTypedData_v4` do not depend on state of network connections, these confirmations do use nicknames/addressbook state which is dependent on globally selected network state for parsing signatures and injecting nicknaming where possible. The queueing system introduced with Amon Hen v1 (v12.0.0 release) introduces certain conditions in which these signature confirmations will be rendered on the wrong network. Though this doesn't actually result in faulty signatures, we should switch to the appropriate/expected network for the UX reasons described above. Adding `eth_signTypedData_v3` and `eth_signTypedData` to the `methodsRequiringNetworkSwitch` array will cause the network to switch to the selected network for the requesting origin before initializing the confirmation. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25562?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/25528 See this slack thread: https://consensys.slack.com/archives/C06FXU326RL/p1719429561925649?thread_ts=1719415715.492249&cid=C06FXU326RL ## **Manual testing steps** See videos below 👇 ## **Screenshots/Recordings** ### **Before** https://github.com/MetaMask/metamask-extension/assets/34557516/3e32fce7-046c-4856-893d-a85083877327 ### **After** https://github.com/MetaMask/metamask-extension/assets/34557516/306d88aa-f5f3-4eae-a8e9-30b621c02626 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- shared/constants/methods-tags.ts | 2 + .../dapp1-send-dapp2-signTypedData.spec.js | 148 ++++++++++++++++++ .../network-account-balance-header.js | 1 + .../confirm-add-suggested-nft.test.js.snap | 2 + .../signature-request-header.test.js.snap | 1 + .../signature-request-original.test.js.snap | 1 + .../signature-request-siwe.test.js.snap | 1 + .../signature-request.test.js.snap | 2 + .../__snapshots__/index.test.js.snap | 1 + .../token-allowance.test.js.snap | 1 + 10 files changed, 160 insertions(+) create mode 100644 test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js diff --git a/shared/constants/methods-tags.ts b/shared/constants/methods-tags.ts index a770fd733cd2..89cce5f67c8d 100644 --- a/shared/constants/methods-tags.ts +++ b/shared/constants/methods-tags.ts @@ -12,6 +12,8 @@ export const methodsRequiringNetworkSwitch = [ 'wallet_switchEthereumChain', 'wallet_addEthereumChain', 'wallet_watchAsset', + 'eth_signTypedData', + 'eth_signTypedData_v3', 'eth_signTypedData_v4', 'personal_sign', ] as const; 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 new file mode 100644 index 000000000000..cd197970baea --- /dev/null +++ b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js @@ -0,0 +1,148 @@ +const FixtureBuilder = require('../../fixture-builder'); +const { + withFixtures, + openDapp, + unlockWallet, + DAPP_URL, + DAPP_ONE_URL, + regularDelayMs, + defaultGanacheOptions, + WINDOW_TITLES, +} = require('../../helpers'); + +describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { + it('should queue signTypedData tx after eth_sendTransaction confirmation and signTypedData confirmation should target the correct network after eth_sendTransaction is confirmed @no-mmi', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Open Dapp One + await openDapp(driver, undefined, DAPP_URL); + + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Open Dapp Two + await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect to dapp 2 + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.switchToWindowWithUrl(DAPP_URL); + + // switch chain for Dapp One + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x3e8' }], + }); + + // Initiate switchEthereumChain on Dapp one + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ text: 'Switch network', tag: 'button' }); + + await driver.switchToWindowWithUrl(DAPP_URL); + + // eth_sendTransaction request + await driver.clickElement('#sendButton'); + + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + + // signTypedData request + await driver.clickElement('#signTypedData'); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // Check correct network on the send confirmation. + await driver.findElement({ + css: '[data-testid="network-display"]', + text: 'Localhost 7777', + }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // Check correct network on the signTypedData confirmation. + await driver.findElement({ + css: '[data-testid="signature-request-network-display"]', + text: 'Localhost 8545', + }); + }, + ); + }); +}); diff --git a/ui/components/app/network-account-balance-header/network-account-balance-header.js b/ui/components/app/network-account-balance-header/network-account-balance-header.js index 98c91acfe81a..9137d53f1f14 100644 --- a/ui/components/app/network-account-balance-header/network-account-balance-header.js +++ b/ui/components/app/network-account-balance-header/network-account-balance-header.js @@ -68,6 +68,7 @@ export default function NetworkAccountBalanceHeader({ variant={TextVariant.bodySm} as="h6" color={TextColor.textAlternative} + data-testid="signature-request-network-display" > {networkName} diff --git a/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap b/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap index eca476209a9c..908df23e73fa 100644 --- a/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap +++ b/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap @@ -83,6 +83,7 @@ exports[`ConfirmAddSuggestedNFT Component should match snapshot 1`] = ` >
Ethereum Mainnet
@@ -295,6 +296,7 @@ exports[`ConfirmAddSuggestedNFT Component should match snapshot 1`] = ` >
Ethereum Mainnet
diff --git a/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap b/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap index 005650a69853..c2df41b7f906 100644 --- a/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-header/__snapshots__/signature-request-header.test.js.snap @@ -71,6 +71,7 @@ exports[`SignatureRequestHeader should match snapshot 1`] = ` >
goerli
diff --git a/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap b/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap index 9ed331fefe28..c9d4d342a59b 100644 --- a/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-original/__snapshots__/signature-request-original.test.js.snap @@ -147,6 +147,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = ` >
goerli
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 a1219a561ba7..5d6a208872d2 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 @@ -144,6 +144,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`] >
goerli
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 5aabba67b7d2..30011e96a607 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 @@ -144,6 +144,7 @@ exports[`Signature Request Component render should match snapshot when we are us >
Localhost 8545
@@ -916,6 +917,7 @@ exports[`Signature Request Component render should match snapshot when we want t >
Localhost 8545
diff --git a/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap b/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap index 1e600744a876..a10be0a7a2b2 100644 --- a/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap +++ b/ui/pages/confirmations/confirm-signature-request/__snapshots__/index.test.js.snap @@ -144,6 +144,7 @@ exports[`Confirm Signature Request Component render should match snapshot 1`] = >
Goerli test network
diff --git a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap index d8ff7862a6da..91ac3c735f84 100644 --- a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap +++ b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap @@ -163,6 +163,7 @@ exports[`TokenAllowancePage when mounted should match snapshot 1`] = ` >
mainnet
From 58082f542b950f979ac362d7aa428f8181e80b9b Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:31:37 -0700 Subject: [PATCH 13/15] chore: add bridge controller, store and api utils (#25044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR doesn't have user-facing effects, just setting up state management for subsequent feature PRs. Changes include * a new bridge controller that manages background state for the cross-chain swaps experience * basic redux slice for bridging. Note that the slice extends swaps, since we plan to decouple the frontend components for now but merge the experiences later on * e2e tests for current bridge button behavior * new logic for fetching and setting bridge feature flags [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25044?quickstart=1) ## **Related issues** Fixes: [METABRIDGE-889](https://consensyssoftware.atlassian.net/browse/METABRIDGE-889) ## **Manual testing steps** 1. Load extension 2. Verify that Bridge button behavior has not changed ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [X] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .eslintrc.js | 1 + .metamaskrc.dist | 1 + app/scripts/controllers/bridge.test.ts | 29 ++++ app/scripts/controllers/bridge.ts | 36 +++++ app/scripts/lib/setupSentry.js | 7 + app/scripts/metamask-controller.js | 8 ++ builds.yml | 1 + shared/constants/bridge.ts | 9 ++ test/e2e/mock-e2e.js | 14 +- .../bridge-click-from-asset-overview.spec.ts | 42 ++++++ .../bridge-click-from-eth-overview.spec.ts | 27 ++++ test/e2e/tests/bridge/bridge-test-utils.ts | 128 ++++++++++++++++++ ...rs-after-init-opt-in-background-state.json | 7 + .../errors-after-init-opt-in-ui-state.json | 6 + test/jest/mock-store.js | 19 +++ ui/ducks/bridge/actions.ts | 9 ++ ui/ducks/bridge/bridge.test.ts | 31 +++++ ui/ducks/bridge/bridge.ts | 25 ++++ ui/ducks/index.js | 2 + ui/ducks/swaps/swaps.js | 1 + ui/hooks/bridge/useBridging.ts | 15 ++ ui/pages/bridge/bridge.util.test.ts | 59 ++++++++ ui/pages/bridge/bridge.util.ts | 72 ++++++++++ ui/store/actions.test.js | 21 +++ ui/store/actions.ts | 11 ++ 25 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 app/scripts/controllers/bridge.test.ts create mode 100644 app/scripts/controllers/bridge.ts create mode 100644 test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts create mode 100644 test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts create mode 100644 test/e2e/tests/bridge/bridge-test-utils.ts create mode 100644 ui/ducks/bridge/actions.ts create mode 100644 ui/ducks/bridge/bridge.test.ts create mode 100644 ui/ducks/bridge/bridge.ts create mode 100644 ui/hooks/bridge/useBridging.ts create mode 100644 ui/pages/bridge/bridge.util.test.ts create mode 100644 ui/pages/bridge/bridge.util.ts diff --git a/.eslintrc.js b/.eslintrc.js index 9f7fed5928ed..0aea1e739e3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -281,6 +281,7 @@ module.exports = { 'app/scripts/controllers/mmi-controller.test.ts', 'app/scripts/metamask-controller.actions.test.js', 'app/scripts/detect-multiple-instances.test.js', + 'app/scripts/controllers/bridge.test.ts', 'app/scripts/controllers/swaps.test.js', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', diff --git a/.metamaskrc.dist b/.metamaskrc.dist index c7431cc0719b..429addc860be 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -6,6 +6,7 @@ INFURA_PROJECT_ID=00000000000 ;PASSWORD=METAMASK PASSWORD ;SEGMENT_WRITE_KEY= +;BRIDGE_USE_DEV_APIS= ;SWAPS_USE_DEV_APIS= ;PORTFOLIO_URL= ;TRANSACTION_SECURITY_PROVIDER= diff --git a/app/scripts/controllers/bridge.test.ts b/app/scripts/controllers/bridge.test.ts new file mode 100644 index 000000000000..a6001f7aa0d7 --- /dev/null +++ b/app/scripts/controllers/bridge.test.ts @@ -0,0 +1,29 @@ +import BridgeController from './bridge'; + +const EMPTY_INIT_STATE = { + bridgeState: { + bridgeFeatureFlags: { + extensionSupport: false, + }, + }, +}; + +describe('BridgeController', function () { + let bridgeController: BridgeController; + + beforeAll(function () { + bridgeController = new BridgeController(); + }); + + it('constructor should setup correctly', function () { + expect(bridgeController.store.getState()).toStrictEqual(EMPTY_INIT_STATE); + }); + + it('setBridgeFeatureFlags should set the bridge feature flags', function () { + const featureFlagsResponse = { extensionSupport: true }; + bridgeController.setBridgeFeatureFlags(featureFlagsResponse); + expect( + bridgeController.store.getState().bridgeState.bridgeFeatureFlags, + ).toStrictEqual(featureFlagsResponse); + }); +}); diff --git a/app/scripts/controllers/bridge.ts b/app/scripts/controllers/bridge.ts new file mode 100644 index 000000000000..23323371ea4c --- /dev/null +++ b/app/scripts/controllers/bridge.ts @@ -0,0 +1,36 @@ +import { ObservableStore } from '@metamask/obs-store'; + +export enum BridgeFeatureFlagsKey { + EXTENSION_SUPPORT = 'extensionSupport', +} + +export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: boolean; +}; + +const initialState = { + bridgeState: { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, + }, + }, +}; + +export default class BridgeController { + store = new ObservableStore(initialState); + + resetState = () => { + this.store.updateState({ + bridgeState: { + ...initialState.bridgeState, + }, + }); + }; + + setBridgeFeatureFlags = (bridgeFeatureFlags: BridgeFeatureFlags) => { + const { bridgeState } = this.store.getState(); + this.store.updateState({ + bridgeState: { ...bridgeState, bridgeFeatureFlags }, + }); + }; +} diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 92173fc5f72c..a6b47eef9376 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -120,6 +120,13 @@ export const SENTRY_BACKGROUND_STATE = { MultichainBalancesController: { balances: false, }, + BridgeController: { + bridgeState: { + bridgeFeatureFlags: { + extensionSupport: false, + }, + }, + }, CronjobController: { jobs: false, }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 360e30a89303..b94c010feda1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -321,6 +321,7 @@ import { createTxVerificationMiddleware } from './lib/tx-verification/tx-verific import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; +import BridgeController from './controllers/bridge'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -1973,6 +1974,7 @@ export default class MetamaskController extends EventEmitter { }, initState.SwapsController, ); + this.bridgeController = new BridgeController(); this.smartTransactionsController = new SmartTransactionsController( { getNetworkClientById: this.networkController.getNetworkClientById.bind( @@ -2202,6 +2204,7 @@ export default class MetamaskController extends EventEmitter { EncryptionPublicKeyController: this.encryptionPublicKeyController, SignatureController: this.signatureController, SwapsController: this.swapsController.store, + BridgeController: this.bridgeController.store, EnsController: this.ensController, ApprovalController: this.approvalController, PPOMController: this.ppomController, @@ -3022,6 +3025,7 @@ export default class MetamaskController extends EventEmitter { appMetadataController, permissionController, preferencesController, + bridgeController, swapsController, tokensController, smartTransactionsController, @@ -3627,6 +3631,10 @@ export default class MetamaskController extends EventEmitter { setSwapsQuotesPollingLimitEnabled: swapsController.setSwapsQuotesPollingLimitEnabled.bind(swapsController), + // Bridge + setBridgeFeatureFlags: + bridgeController.setBridgeFeatureFlags.bind(bridgeController), + // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( smartTransactionsController, diff --git a/builds.yml b/builds.yml index aa93f756db49..0e83d86303b0 100644 --- a/builds.yml +++ b/builds.yml @@ -135,6 +135,7 @@ features: # env object supports both declarations (- FOO), and definitions (- FOO: BAR). # Variables that were declared have to be defined somewhere in the load chain before usage env: + - BRIDGE_USE_DEV_APIS: false - SWAPS_USE_DEV_APIS: false - PORTFOLIO_URL: https://portfolio.metamask.io - TOKEN_ALLOWANCE_IMPROVEMENTS: false diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index e02c992bbbba..6ac9cfd4245e 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -1,5 +1,6 @@ import { CHAIN_IDS } from './network'; +// TODO read from feature flags export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.MAINNET, CHAIN_IDS.BSC, @@ -11,3 +12,11 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.BASE, ]; + +const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; +const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; +export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS + ? BRIDGE_DEV_API_BASE_URL + : BRIDGE_PROD_API_BASE_URL; + +export const BRIDGE_CLIENT_ID = 'extension'; diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index f3279377114d..50bd7633f8d3 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -629,6 +629,15 @@ async function setupMocking( return [...privacyReport].sort(); } + /** + * Excludes hosts from the privacyReport if they are refered to by the MetaMask Portfolio + * in a different tab. This is because the Portfolio is a separate application + * + * @param request + */ + const portfolioRequestsMatcher = (request) => + request.headers.referer === 'https://portfolio.metamask.io/'; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -638,7 +647,10 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { - if (request.headers.host.match(browserAPIRequestDomains) === null) { + if ( + request.headers.host.match(browserAPIRequestDomains) === null && + !portfolioRequestsMatcher(request) + ) { privacyReport.add(request.headers.host); } }); diff --git a/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts b/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts new file mode 100644 index 000000000000..24f0bc0fb233 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts @@ -0,0 +1,42 @@ +import { Suite } from 'mocha'; +import { withFixtures, logInWithBalanceValidation } from '../../helpers'; +import { Ganache } from '../../seeder/ganache'; +import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../webdriver/driver'; +import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; + +describe('Click bridge button from asset page @no-mmi', function (this: Suite) { + it('loads portfolio tab when flag is turned off', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle()), + async ({ + driver, + ganacheServer, + contractRegistry, + }: { + driver: Driver; + ganacheServer: Ganache; + contractRegistry: GanacheContractAddressRegistry; + }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + + // ETH + await bridgePage.loadAssetPage(contractRegistry); + await bridgePage.load('coin-overview'); + await bridgePage.verifyPortfolioTab( + 'https://portfolio.metamask.io/bridge?metametricsId=null', + ); + + await bridgePage.reloadHome(); + + // TST + await bridgePage.loadAssetPage(contractRegistry, 'TST'); + await bridgePage.load('token-overview'); + await bridgePage.verifyPortfolioTab( + 'https://portfolio.metamask.io/bridge?metametricsId=null', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts b/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts new file mode 100644 index 000000000000..0a6098e01592 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts @@ -0,0 +1,27 @@ +import { Suite } from 'mocha'; +import { withFixtures, logInWithBalanceValidation } from '../../helpers'; +import { Ganache } from '../../seeder/ganache'; +import { Driver } from '../../webdriver/driver'; +import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; + +describe('Click bridge button from wallet overview @no-mmi', function (this: Suite) { + it('loads portfolio tab when flag is turned off', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle()), + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer: Ganache; + }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + await bridgePage.load(); + await bridgePage.verifyPortfolioTab( + 'https://portfolio.metamask.io/bridge?metametricsId=null', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts new file mode 100644 index 000000000000..157876f43769 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -0,0 +1,128 @@ +import { strict as assert } from 'assert'; +import { Mockttp } from 'mockttp'; +import FixtureBuilder from '../../fixture-builder'; +import { + WINDOW_TITLES, + clickNestedButton, + generateGanacheOptions, +} from '../../helpers'; +import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import GanacheContractAddressRegistry from '../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../webdriver/driver'; + +export class BridgePage { + driver: Driver; + + constructor(driver: Driver) { + this.driver = driver; + } + + load = async ( + location: + | 'wallet-overview' + | 'coin-overview' + | 'token-overview' = 'wallet-overview', + ) => { + let bridgeButtonTestIdPrefix; + switch (location) { + case 'wallet-overview': + bridgeButtonTestIdPrefix = 'eth'; + break; + case 'coin-overview': // native asset page + bridgeButtonTestIdPrefix = 'coin'; + break; + case 'token-overview': + default: + bridgeButtonTestIdPrefix = 'token'; + } + await this.driver.clickElement( + `[data-testid="${bridgeButtonTestIdPrefix}-overview-bridge"]`, + ); + }; + + reloadHome = async (shouldCloseWindow = true) => { + if (shouldCloseWindow) { + await this.driver.closeWindow(); + await this.driver.delay(2000); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + } + await this.driver.navigate(); + }; + + loadAssetPage = async ( + contractRegistry: GanacheContractAddressRegistry, + symbol?: string, + ) => { + let tokenListItem; + + if (symbol) { + // Import token + const contractAddress = await contractRegistry.getContractAddress( + SMART_CONTRACTS.HST, + ); + await this.driver.clickElement({ + text: 'Import tokens', + tag: 'button', + }); + await clickNestedButton(this.driver, 'Custom token'); + await this.driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + contractAddress, + ); + await this.driver.waitForSelector( + '[data-testid="import-tokens-modal-custom-decimals"]', + ); + await this.driver.clickElement({ + text: 'Next', + tag: 'button', + }); + await this.driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); + await this.driver.delay(2000); + tokenListItem = await this.driver.findElement({ text: symbol }); + } else { + tokenListItem = await this.driver.findElement( + '[data-testid="multichain-token-list-button"]', + ); + } + await tokenListItem.click(); + assert.ok((await this.driver.getCurrentUrl()).includes('asset')); + }; + + verifyPortfolioTab = async (url: string) => { + await this.driver.delay(4000); + await this.driver.switchToWindowWithTitle('MetaMask Portfolio - Bridge'); + assert.equal(await this.driver.getCurrentUrl(), url); + }; + + verifySwapPage = async () => { + const currentUrl = await this.driver.getCurrentUrl(); + assert.ok(currentUrl.includes('cross-chain/swaps')); + }; +} + +export const getBridgeFixtures = ( + title?: string, + testSpecificMock?: (server: Mockttp) => Promise, +) => { + return { + driverOptions: { + openDevToolsForTabs: true, + }, + fixtures: new FixtureBuilder({ inputChainId: CHAIN_IDS.MAINNET }) + .withNetworkControllerOnMainnet() + .withTokensControllerERC20() + .build(), + testSpecificMock, + smartContract: SMART_CONTRACTS.HST, + ganacheOptions: generateGanacheOptions({ + hardfork: 'london', + chain: { chainId: CHAIN_IDS.MAINNET }, + }), + title, + }; +}; 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 84fef8e26a90..a7169143d03e 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 @@ -59,6 +59,13 @@ "approvalFlows": "object" }, "AuthenticationController": { "isSignedIn": "boolean" }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean" + } + } + }, "CronjobController": { "jobs": "object" }, "CurrencyController": { "currencyRates": { 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 4b7972acd311..a79a2e543573 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 @@ -2,6 +2,7 @@ "DNS": "object", "activeTab": "object", "appState": "object", + "bridge": "object", "confirm": "object", "confirmAlerts": "object", "confirmTransaction": "object", @@ -60,6 +61,11 @@ }, "connectedStatusPopoverHasBeenShown": true, "defaultHomeActiveTabName": null, + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean" + } + }, "browserEnvironment": { "os": "string", "browser": "string" }, "popupGasPollTokens": "object", "notificationGasPollTokens": "object", diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 365d9b00bdbd..85f617450136 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -666,3 +666,22 @@ export const createSwapsMockStore = () => { }, }; }; + +export const createBridgeMockStore = () => { + const swapsStore = createSwapsMockStore(); + return { + ...swapsStore, + bridge: { + toChain: null, + }, + metamask: { + ...swapsStore.metamask, + bridgeState: { + ...(swapsStore.metamask.bridgeState ?? {}), + bridgeFeatureFlags: { + extensionSupport: false, + }, + }, + }, + }; +}; diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts new file mode 100644 index 000000000000..a9ce6f825b0f --- /dev/null +++ b/ui/ducks/bridge/actions.ts @@ -0,0 +1,9 @@ +import { swapsSlice } from '../swaps/swaps'; +import { bridgeSlice } from './bridge'; + +// Bridge actions + +// eslint-disable-next-line no-empty-pattern +const {} = swapsSlice.actions; + +export const { setToChain } = bridgeSlice.actions; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts new file mode 100644 index 000000000000..5aff92b54251 --- /dev/null +++ b/ui/ducks/bridge/bridge.test.ts @@ -0,0 +1,31 @@ +import nock from 'nock'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import bridgeReducer from './bridge'; +import { setToChain } from './actions'; + +const middleware = [thunk]; + +describe('Ducks - Bridge', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const store = configureMockStore(middleware)(createBridgeMockStore()); + + afterEach(() => { + nock.cleanAll(); + }); + + describe('setToChain', () => { + it('calls the "bridge/setToChain" action', () => { + const state = store.getState().bridge; + const actionPayload = CHAIN_IDS.BSC; + store.dispatch(setToChain(actionPayload)); + const actions = store.getActions(); + expect(actions[0].type).toBe('bridge/setToChain'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toChain).toBe(actionPayload); + }); + }); +}); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts new file mode 100644 index 000000000000..2f6b4aabd775 --- /dev/null +++ b/ui/ducks/bridge/bridge.ts @@ -0,0 +1,25 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { swapsSlice } from '../swaps/swaps'; + +// Only states that are not in swaps slice +type BridgeState = { + toChain: string | null; +}; + +const initialState: BridgeState = { + toChain: null, +}; + +const bridgeSlice = createSlice({ + name: 'bridge', + initialState: { ...initialState }, + reducers: { + ...swapsSlice.reducer, + setToChain: (state, action) => { + state.toChain = action.payload; + }, + }, +}); + +export { bridgeSlice }; +export default bridgeSlice.reducer; diff --git a/ui/ducks/index.js b/ui/ducks/index.js index f72918460655..069bd385c093 100644 --- a/ui/ducks/index.js +++ b/ui/ducks/index.js @@ -10,6 +10,7 @@ import confirmTransactionReducer from './confirm-transaction/confirm-transaction import gasReducer from './gas/gas.duck'; import { invalidCustomNetwork, unconnectedAccount } from './alerts'; import swapsReducer from './swaps/swaps'; +import bridgeReducer from './bridge/bridge'; import historyReducer from './history/history'; import rampsReducer from './ramps/ramps'; import confirmAlertsReducer from './confirm-alerts/confirm-alerts'; @@ -28,6 +29,7 @@ export default combineReducers({ confirmTransaction: confirmTransactionReducer, swaps: swapsReducer, ramps: rampsReducer, + bridge: bridgeReducer, gas: gasReducer, localeMessages: localeMessagesReducer, }); diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 59fadda433d5..9c15aec7435b 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -511,6 +511,7 @@ export { swapCustomGasModalLimitEdited, swapCustomGasModalClosed, setTransactionSettingsOpened, + slice as swapsSlice, }; export const navigateBackToBuildQuote = (history) => { diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts new file mode 100644 index 000000000000..9e6356d99941 --- /dev/null +++ b/ui/hooks/bridge/useBridging.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { fetchBridgeFeatureFlags } from '../../pages/bridge/bridge.util'; +import { setBridgeFeatureFlags } from '../../store/actions'; + +const useBridging = () => { + const dispatch = useDispatch(); + + useEffect(() => { + fetchBridgeFeatureFlags().then((bridgeFeatureFlags) => { + dispatch(setBridgeFeatureFlags(bridgeFeatureFlags)); + }); + }, [dispatch, setBridgeFeatureFlags]); +}; +export default useBridging; diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts new file mode 100644 index 000000000000..d693f92dc956 --- /dev/null +++ b/ui/pages/bridge/bridge.util.test.ts @@ -0,0 +1,59 @@ +import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +import { fetchBridgeFeatureFlags } from './bridge.util'; + +jest.mock('../../../shared/lib/fetch-with-cache'); + +describe('Bridge utils', () => { + it('should fetch bridge feature flags successfully', async () => { + const mockResponse = { + 'extension-support': true, + }; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags(); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeFeatureFlags', + }); + + expect(result).toEqual({ extensionSupport: true }); + }); + + it('should use fallback bridge feature flags if response is unexpected', async () => { + const mockResponse = { + flag1: true, + flag2: false, + }; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags(); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeFeatureFlags', + }); + + expect(result).toEqual({ extensionSupport: false }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (fetchWithCache as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeFeatureFlags()).rejects.toThrowError(mockError); + }); +}); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts new file mode 100644 index 000000000000..d7fae190e28f --- /dev/null +++ b/ui/pages/bridge/bridge.util.ts @@ -0,0 +1,72 @@ +import { + BridgeFeatureFlagsKey, + BridgeFeatureFlags, +} from '../../../app/scripts/controllers/bridge'; +import { + BRIDGE_API_BASE_URL, + BRIDGE_CLIENT_ID, +} from '../../../shared/constants/bridge'; +import { MINUTE } from '../../../shared/constants/time'; +import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +import { validateData } from '../../../shared/lib/swaps-utils'; + +const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; +const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; + +// Types copied from Metabridge API +enum BridgeFlag { + EXTENSION_SUPPORT = 'extension-support', +} + +type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_SUPPORT]: boolean; +}; +// End of copied types + +type Validator = { + property: keyof T; + type: string; + validator: (value: unknown) => boolean; +}; + +const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, +): data is T => { + return validateData(validators, data, urlUsed); +}; + +export async function fetchBridgeFeatureFlags(): Promise { + const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; + const rawFeatureFlags = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeFeatureFlags', + }); + + if ( + validateResponse( + [ + { + property: BridgeFlag.EXTENSION_SUPPORT, + type: 'boolean', + validator: (v) => typeof v === 'boolean', + }, + ], + rawFeatureFlags, + url, + ) + ) { + return { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: + rawFeatureFlags[BridgeFlag.EXTENSION_SUPPORT], + }; + } + + return { + // TODO set default to true once bridging is live + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, + }; +} diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index aa161ba781a1..255d0b26adce 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -2690,4 +2690,25 @@ describe('Actions', () => { expect(store.getActions()).toStrictEqual(expectedActions); }); }); + + describe('#setBridgeFeatureFlags', () => { + it('calls setBridgeFeatureFlags in the background', async () => { + const store = mockStore(); + background.setBridgeFeatureFlags = sinon + .stub() + .callsFake((_, cb) => cb()); + setBackgroundConnection(background); + + await store.dispatch( + actions.setBridgeFeatureFlags({ extensionSupport: true }), + ); + + expect(background.setBridgeFeatureFlags.callCount).toStrictEqual(1); + expect(background.setBridgeFeatureFlags.getCall(0).args[0]).toStrictEqual( + { + extensionSupport: true, + }, + ); + }); + }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 186e59ccb8df..1bc6a437f121 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -113,6 +113,7 @@ import { import { ThemeType } from '../../shared/constants/preferences'; import { FirstTimeFlowType } from '../../shared/constants/onboarding'; import type { MarkAsReadNotificationsParam } from '../../app/scripts/controllers/metamask-notifications/types/notification/notification'; +import { BridgeFeatureFlags } from '../../app/scripts/controllers/bridge'; import * as actionConstants from './actionConstants'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { updateCustodyState } from './institutional/institution-actions'; @@ -3889,6 +3890,16 @@ export function setInitialGasEstimate( }; } +// Bridge +export function setBridgeFeatureFlags( + featureFlags: BridgeFeatureFlags, +): ThunkAction { + return async (dispatch: MetaMaskReduxDispatch) => { + await submitRequestToBackground('setBridgeFeatureFlags', [featureFlags]); + await forceUpdateMetamaskState(dispatch); + }; +} + // Permissions export function requestAccountsPermissionWithId( From f353f0c316a18510a71ac3c4d45b765f78475028 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Sat, 29 Jun 2024 06:42:08 +0800 Subject: [PATCH 14/15] chore(deps): bump assets controller to v34.0.0 (#25540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/assets-controllers` to `^34.0.0`. ## **Related issues** Fixes https://github.com/MetaMask/accounts-planning/issues/481 ## **Manual testing steps** This PR affects all assets related tokens 1. Test added and removing of and tokens 2. Transfer tokens 3. Turn on off token detection and see the detected tokens 4. Check if the token rate values on mainnet ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- app/scripts/metamask-controller.js | 150 ++++++------------------ app/scripts/metamask-controller.test.js | 103 +++++++--------- lavamoat/browserify/beta/policy.json | 1 - lavamoat/browserify/flask/policy.json | 1 - lavamoat/browserify/main/policy.json | 1 - lavamoat/browserify/mmi/policy.json | 1 - package.json | 2 +- yarn.lock | 21 ++-- 8 files changed, 92 insertions(+), 188 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b94c010feda1..88491e2af62c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -524,12 +524,6 @@ export default class MetamaskController extends EventEmitter { this.networkController.getProviderAndBlockTracker().blockTracker; this.deprecatedNetworkVersions = {}; - const tokenListMessenger = this.controllerMessenger.getRestricted({ - name: 'TokenListController', - allowedEvents: ['NetworkController:stateChange'], - allowedActions: ['NetworkController:getNetworkClientById'], - }); - const accountsControllerMessenger = this.controllerMessenger.getRestricted({ name: 'AccountsController', allowedEvents: [ @@ -567,6 +561,12 @@ export default class MetamaskController extends EventEmitter { networkConfigurations: this.networkController.state.networkConfigurations, }); + const tokenListMessenger = this.controllerMessenger.getRestricted({ + name: 'TokenListController', + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: ['NetworkController:stateChange'], + }); + this.tokenListController = new TokenListController({ chainId: this.networkController.state.providerConfig.chainId, preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( @@ -604,49 +604,21 @@ export default class MetamaskController extends EventEmitter { allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getNetworkClientById', + 'AccountsController:getSelectedAccount', + 'AccountsController:getAccount', ], allowedEvents: [ 'NetworkController:networkDidChange', - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'PreferencesController:stateChange', 'TokenListController:stateChange', ], }); this.tokensController = new TokensController({ + state: initState.TokensController, + provider: this.provider, messenger: tokensControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - // TODO: The tokens controller currently does not support internalAccounts. This is done to match the behavior of the previous tokens controller subscription. - onPreferencesStateChange: (listener) => - this.controllerMessenger.subscribe( - `AccountsController:selectedAccountChange`, - (newlySelectedInternalAccount) => { - listener({ selectedAddress: newlySelectedInternalAccount.address }); - }, - ), - onNetworkDidChange: (cb) => - networkControllerMessenger.subscribe( - 'NetworkController:networkDidChange', - () => { - const networkState = this.networkController.state; - return cb(networkState); - }, - ), - onTokenListStateChange: (listener) => - this.controllerMessenger.subscribe( - `${this.tokenListController.name}:stateChange`, - listener, - ), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - config: { - provider: this.provider, - selectedAddress: - initState.AccountsController?.internalAccounts?.accounts[ - initState.AccountsController?.internalAccounts?.selectedAccount - ]?.address ?? '', - }, - state: initState.TokensController, }); const nftControllerMessenger = this.controllerMessenger.getRestricted({ @@ -664,15 +636,9 @@ export default class MetamaskController extends EventEmitter { ], }); this.nftController = new NftController({ + state: initState.NftController, messenger: nftControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( this.assetsContractController, ), @@ -706,10 +672,6 @@ export default class MetamaskController extends EventEmitter { source, }, }), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - state: initState.NftController, }); this.nftController.setApiKey(process.env.OPENSEA_KEY); @@ -718,14 +680,13 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: 'NftDetectionController', allowedEvents: [ - 'PreferencesController:stateChange', 'NetworkController:stateChange', + 'PreferencesController:stateChange', ], allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getState', 'NetworkController:getNetworkClientById', - 'PreferencesController:getState', 'AccountsController:getSelectedAccount', ], }); @@ -733,21 +694,12 @@ export default class MetamaskController extends EventEmitter { this.nftDetectionController = new NftDetectionController({ messenger: nftDetectionControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - onNftsStateChange: (listener) => this.nftController.subscribe(listener), - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), getOpenSeaApiKey: () => this.nftController.openSeaApiKey, getBalancesInSingleCall: this.assetsContractController.getBalancesInSingleCall.bind( this.assetsContractController, ), addNft: this.nftController.addNft.bind(this.nftController), - getNftApi: this.nftController.getNftApi.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] disabled: @@ -755,11 +707,6 @@ export default class MetamaskController extends EventEmitter { undefined ? false // the detection is enabled by default : !this.preferencesController.store.getState().useNftDetection, - selectedAddress: - this.preferencesController.store.getState().selectedAddress, - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), }); this.metaMetricsController = new MetaMetricsController({ @@ -953,55 +900,29 @@ export default class MetamaskController extends EventEmitter { fetchMultiExchangeRate, }); - const tokenRatesControllerMessenger = - this.controllerMessenger.getRestricted({ - name: 'TokenRatesController', - allowedEvents: [ - 'PreferencesController:stateChange', - 'TokensController:stateChange', - 'NetworkController:stateChange', - ], - allowedActions: [ - 'TokensController:getState', - 'NetworkController:getNetworkClientById', - 'NetworkController:getState', - 'PreferencesController:getState', - ], - }); + const tokenRatesMessenger = this.controllerMessenger.getRestricted({ + name: 'TokenRatesController', + allowedActions: [ + 'TokensController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + 'AccountsController:getAccount', + 'AccountsController:getSelectedAccount', + ], + allowedEvents: [ + 'NetworkController:stateChange', + 'AccountsController:selectedEvmAccountChange', + 'PreferencesController:stateChange', + 'TokensController:stateChange', + ], + }); // token exchange rate tracker - this.tokenRatesController = new TokenRatesController( - { - messenger: tokenRatesControllerMessenger, - chainId: this.networkController.state.providerConfig.chainId, - ticker: this.networkController.state.providerConfig.ticker, - selectedAddress: this.accountsController.getSelectedAccount().address, - onTokensStateChange: (listener) => - this.tokensController.subscribe(listener), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), - onPreferencesStateChange: (listener) => - this.controllerMessenger.subscribe( - `AccountsController:selectedAccountChange`, - (newlySelectedInternalAccount) => { - listener({ - selectedAddress: newlySelectedInternalAccount.address, - }); - }, - ), - tokenPricesService: new CodefiTokenPricesServiceV2(), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - }, - { - allTokens: this.tokensController.state.allTokens, - allDetectedTokens: this.tokensController.state.allDetectedTokens, - }, - initState.TokenRatesController, - ); + this.tokenRatesController = new TokenRatesController({ + state: initState.TokenRatesController, + messenger: tokenRatesMessenger, + tokenPricesService: new CodefiTokenPricesServiceV2(), + }); this.preferencesController.store.subscribe( previousValueComparator((prevState, currState) => { @@ -1628,6 +1549,7 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: 'TokenDetectionController', allowedActions: [ + 'AccountsController:getAccount', 'AccountsController:getSelectedAccount', 'KeyringController:getState', 'NetworkController:getNetworkClientById', @@ -1639,7 +1561,7 @@ export default class MetamaskController extends EventEmitter { 'TokensController:addDetectedTokens', ], allowedEvents: [ - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'KeyringController:lock', 'KeyringController:unlock', 'NetworkController:networkDidChange', diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 771378568020..1ad12736fe2a 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -662,59 +662,46 @@ describe('MetaMaskController', () => { it('should clear previous identities after vault restoration', async () => { jest.spyOn(metamaskController, 'getBalance').mockResolvedValue('0x0'); - let startTime = Date.now(); await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED, ); - let endTime = Date.now(); - const firstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const firstVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); - expect( - firstVaultIdentities[TEST_ADDRESS].lastSelected >= startTime && - firstVaultIdentities[TEST_ADDRESS].lastSelected <= endTime, - ).toStrictEqual(true); - delete firstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(firstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - }); + expect(firstVaultAccounts).toHaveLength(1); + expect(firstVaultAccounts[0].address).toBe(TEST_ADDRESS); - await metamaskController.preferencesController.setAccountLabel( - TEST_ADDRESS, + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + metamaskController.accountsController.setAccountName( + selectedAccount.id, 'Account Foo', ); - const labelledFirstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const labelledFirstVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); - delete labelledFirstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(labelledFirstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, - }); - startTime = Date.now(); + expect(labelledFirstVaultAccounts[0].address).toBe(TEST_ADDRESS); + expect(labelledFirstVaultAccounts[0].metadata.name).toBe('Account Foo'); + await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED_ALT, ); - endTime = Date.now(); - const secondVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const secondVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); + + expect(secondVaultAccounts).toHaveLength(1); expect( - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected >= startTime && - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected <= endTime, - ).toStrictEqual(true); - delete secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected; - expect(secondVaultIdentities).toStrictEqual({ - [TEST_ADDRESS_ALT]: { - address: TEST_ADDRESS_ALT, - name: DEFAULT_LABEL, - }, - }); + metamaskController.accountsController.getSelectedAccount().address, + ).toBe(TEST_ADDRESS_ALT); + expect(secondVaultAccounts[0].address).toBe(TEST_ADDRESS_ALT); + expect(secondVaultAccounts[0].metadata.name).toBe(DEFAULT_LABEL); }); it('should restore any consecutive accounts with balances without extra zero balance accounts', async () => { @@ -748,29 +735,29 @@ describe('MetaMaskController', () => { allDetectedTokens: { '0x1': { [TEST_ADDRESS_2]: [{}] } }, }); - const startTime = Date.now(); await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED, ); // Expect first account to be selected - const identities = cloneDeep(metamaskController.getState().identities); - expect( - identities[TEST_ADDRESS].lastSelected >= startTime && - identities[TEST_ADDRESS].lastSelected <= Date.now(), - ).toStrictEqual(true); - - // Expect first 2 accounts to be restored - delete identities[TEST_ADDRESS].lastSelected; - expect(identities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - [TEST_ADDRESS_2]: { - address: TEST_ADDRESS_2, - name: 'Account 2', - lastSelected: expect.any(Number), - }, - }); + const accounts = cloneDeep( + metamaskController.accountsController.listAccounts(), + ); + + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + + expect(selectedAccount.address).toBe(TEST_ADDRESS); + expect(accounts).toHaveLength(2); + expect(accounts[0].address).toBe(TEST_ADDRESS); + expect(accounts[0].metadata.name).toBe(DEFAULT_LABEL); + expect(accounts[1].address).toBe(TEST_ADDRESS_2); + expect(accounts[1].metadata.name).toBe('Account 2'); + // TODO: Handle last selected in the update of the next accounts controller. + // expect(accounts[1].metadata.lastSelected).toBeGreaterThan( + // accounts[0].metadata.lastSelected, + // ); }); }); @@ -1596,14 +1583,12 @@ describe('MetaMaskController', () => { symbol: 'FOO', }; - metamaskController.tokensController.update((state) => { - state.tokens = [ - { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - ...tokenData, - }, - ]; - }); + await metamaskController.tokensController.addTokens([ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + ...tokenData, + }, + ]); metamaskController.provider = provider; const tokenDetails = diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index d17b42a08fc8..6e8a55b7124a 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -800,7 +800,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index d17b42a08fc8..6e8a55b7124a 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -800,7 +800,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index d17b42a08fc8..6e8a55b7124a 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -800,7 +800,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index b1576a7d4f79..08b8c7e19958 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -892,7 +892,6 @@ "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/snaps-utils": true, "@metamask/utils": true, "uuid": true } diff --git a/package.json b/package.json index 0eb9a1d6fdc8..f9dbb8dbeb77 100644 --- a/package.json +++ b/package.json @@ -289,7 +289,7 @@ "@metamask/address-book-controller": "^4.0.1", "@metamask/announcement-controller": "^6.1.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "^33.0.0", + "@metamask/assets-controllers": "^34.0.0", "@metamask/base-controller": "^5.0.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", diff --git a/yarn.lock b/yarn.lock index 6658ee69abfe..1b0c258c8e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4751,14 +4751,15 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^17.0.0": - version: 17.0.0 - resolution: "@metamask/accounts-controller@npm:17.0.0" +"@metamask/accounts-controller@npm:^17.0.0, @metamask/accounts-controller@npm:^17.1.0": + version: 17.1.1 + resolution: "@metamask/accounts-controller@npm:17.1.1" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/base-controller": "npm:^6.0.0" "@metamask/eth-snap-keyring": "npm:^4.3.1" "@metamask/keyring-api": "npm:^8.0.0" + "@metamask/keyring-controller": "npm:^17.1.0" "@metamask/snaps-sdk": "npm:^4.2.0" "@metamask/snaps-utils": "npm:^7.4.0" "@metamask/utils": "npm:^8.3.0" @@ -4769,7 +4770,7 @@ __metadata: peerDependencies: "@metamask/keyring-controller": ^17.0.0 "@metamask/snaps-controllers": ^8.1.1 - checksum: 10/49ff64d252a463e00d0ad1baad6ac1c2fea9660899c7519c4ce3bc52dcf856d62094b141aaa5ae358b2f26b58d919db4820317c72b66a221656e35a86e55d579 + checksum: 10/79c74f1e219d616ffa5754418b27e2a6f2704ed0690201b46fdcffba5b25297decee735587e373b65b31f6f1817ad1d0b3072525335f67c2da1b4ff25b077b9c languageName: node linkType: hard @@ -4824,9 +4825,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^33.0.0": - version: 33.0.0 - resolution: "@metamask/assets-controllers@npm:33.0.0" +"@metamask/assets-controllers@npm:^34.0.0": + version: 34.0.0 + resolution: "@metamask/assets-controllers@npm:34.0.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4834,7 +4835,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.2" - "@metamask/accounts-controller": "npm:^17.0.0" + "@metamask/accounts-controller": "npm:^17.1.0" "@metamask/approval-controller": "npm:^7.0.0" "@metamask/base-controller": "npm:^6.0.0" "@metamask/contract-metadata": "npm:^2.4.0" @@ -4862,7 +4863,7 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^19.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/cb35e1a170c10f64df023938108593df3c5686e92070593f230c1146cd78d5ed4fbff9027cb18920c11a1ac2dc1090ce718ed22ba61dfd337fe68c18f4c06164 + checksum: 10/181cdfbcceb71ffa6d9126d70ebe97cee43ddcc1a50554594cea073d127a3a9ddc0666615b462563e33700d32b9b405cecc8a44fbcd95c84eb3b6053546ab480 languageName: node linkType: hard @@ -25170,7 +25171,7 @@ __metadata: "@metamask/announcement-controller": "npm:^6.1.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "npm:^33.0.0" + "@metamask/assets-controllers": "npm:^34.0.0" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^5.0.1" "@metamask/browser-passworder": "npm:^4.3.0" From 3ea381d26963849a33d76d5b644c5f0044c9c778 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:17:11 +0200 Subject: [PATCH 15/15] chore: adds quality gate for rerunning e2e spec files that are new or have been modified (#24556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds a quality gate for new or modified e2e spec files. Whenever there is a PR which modifies or changes a test, this will be run more times, in order to prevent introducing a flakiness accidentally. It is done as follows: - Identifies any new or modified e2e file from inside the test/ folder using `git diff` and using these 2 filters: - `file.filename.startsWith('test/e2e/') &&` - `file.filename.endsWith('.spec.js') || file.filename.endsWith('.spec.ts') ` - Copies the given specs x5 times in the list of testpaths to execute -> this number is arbitrary, we could modify it to any value we want. The reason for taking this approach instead of changing the retrial number is to benefit of the parallelization, as @HowardBraham pointed out in a comment. - Since we already had a flag which could support the re-running successful tests, `--retry-until-failure` I just leveraged this into the `for` loop for each test, and if that testcase was identified as new/modified, the flag is added so the new tests fail fast without retrials ### Incremental git fetch depth within shallow clone We use git fetch with incremental depth as @danjm suggested. The ci environment uses a shallow clone, meaning we won't be able to succeed just by using git diff as it won't find the merge base. For fixing that, we start with a git fetch depth of 1, and keep incrementing the depth (1, 10, 100) it the error is `no merge base` up until 100. If the git diff still fails, we then do a full git fetch with the `unshallow` flag. - [Update] This is the working result with the last commit https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/89269/workflows/103b78a8-8f0d-4323-96b0-9e235c4cbc81/jobs/3296802 ![Screenshot from 2024-06-26 11-39-19](https://github.com/MetaMask/metamask-extension/assets/54408225/a2a89d6a-3a73-48ba-91a3-20aeadc38573) ### New ci Job The git diff is done in a new ci job which runs at the beginning in parallel of prep-deps. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24556?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/24009 ## **Manual testing steps** 1. Check ci runs (notice previous runs had failing and changed tests on purpose, in order to try the different scenarios described below) ## **Screenshots/Recordings** =============================================== [UPDATE with the new code changes] - :green_circle: Case 1: A test has changed -> it's rerun 1+5 times and it's successful (it will be run in different buckets) https://github.com/MetaMask/metamask-extension/assets/54408225/c1456104-1f5f-4ef3-9364-4e435f8797f4 https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/89277/workflows/7fce0a2e-773f-46da-8ab9-1dbec7992b58/jobs/3297267/parallel-runs/10?filterBy=ALL - :green_circle: Case 2: A test has changed, but it has a mistake in the code (intentionally to simulate a flaky test) -> it fails immediately and there are no more retries. The rest of the tests, are retried if they failed as usual - Case for main test build test: https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/89277/workflows/7fce0a2e-773f-46da-8ab9-1dbec7992b58/jobs/3297267/artifacts - Case for flask specific test: https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/89277/workflows/7fce0a2e-773f-46da-8ab9-1dbec7992b58/jobs/3297277/artifacts - :green_circle: Case 3: A PR has no test spec files changed -> nothing different happens - ci run: check current ci ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Mark Stacey Co-authored-by: Howard Braham --- .circleci/config.yml | 31 +++++++++ .circleci/scripts/git-diff-develop.ts | 99 +++++++++++++++++++++++++++ development/lib/retry.js | 20 +++--- test/e2e/changedFilesUtil.js | 44 ++++++++++++ test/e2e/run-all.js | 62 +++++++++++++++-- test/e2e/run-e2e-test.js | 6 +- 6 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 .circleci/scripts/git-diff-develop.ts create mode 100644 test/e2e/changedFilesUtil.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ab0af8fb0cc..d9f0754335e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,6 +105,9 @@ workflows: - prep-deps - check-pr-tag - prep-deps + - get-changed-files-with-git-diff: + requires: + - prep-deps - test-deps-audit: requires: - prep-deps @@ -187,41 +190,51 @@ workflows: - test-e2e-chrome: requires: - prep-build-test + - get-changed-files-with-git-diff - test-e2e-chrome-confirmation-redesign: requires: - prep-build-confirmation-redesign-test + - get-changed-files-with-git-diff - test-e2e-firefox: requires: - prep-build-test-mv2 + - get-changed-files-with-git-diff - test-e2e-firefox-confirmation-redesign: <<: *develop_master_rc_only requires: - prep-build-confirmation-redesign-test-mv2 + - get-changed-files-with-git-diff - test-e2e-chrome-rpc: requires: - prep-build-test + - get-changed-files-with-git-diff - test-api-specs: requires: - prep-build-test - test-e2e-chrome-multiple-providers: requires: - prep-build-test + - get-changed-files-with-git-diff - test-e2e-chrome-flask: requires: - prep-build-test-flask + - get-changed-files-with-git-diff - test-e2e-firefox-flask: <<: *develop_master_rc_only requires: - prep-build-test-flask-mv2 + - get-changed-files-with-git-diff - test-e2e-chrome-mmi: requires: - prep-build-test-mmi + - get-changed-files-with-git-diff - test-e2e-mmi-playwright - OPTIONAL: requires: - prep-build-test-mmi-playwright - test-e2e-chrome-rpc-mmi: requires: - prep-build-test-mmi + - get-changed-files-with-git-diff - test-e2e-chrome-vault-decryption: filters: branches: @@ -230,6 +243,7 @@ workflows: - /^Version-v(\d+)[.](\d+)[.](\d+)/ requires: - prep-build + - get-changed-files-with-git-diff - test-unit-jest-main: requires: - prep-deps @@ -472,6 +486,23 @@ jobs: - node_modules - build-artifacts + # This job is used for the e2e quality gate. + # It must be run before any job which uses the run-all.js script. + get-changed-files-with-git-diff: + executor: node-browsers-small + steps: + - run: *shallow-git-clone + - run: sudo corepack enable + - attach_workspace: + at: . + - run: + name: Get changed files with git diff + command: npx tsx .circleci/scripts/git-diff-develop.ts + - persist_to_workspace: + root: . + paths: + - changed-files + validate-lavamoat-allow-scripts: executor: node-browsers-small steps: diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts new file mode 100644 index 000000000000..8b5680b17d3f --- /dev/null +++ b/.circleci/scripts/git-diff-develop.ts @@ -0,0 +1,99 @@ +import { hasProperty } from '@metamask/utils'; +import { exec as execCallback } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; + +const exec = promisify(execCallback); + +/** + * Fetches the git repository with a specified depth. + * + * @param depth - The depth to use for the fetch command. + * @returns True if the fetch is successful, otherwise false. + */ +async function fetchWithDepth(depth: number): Promise { + try { + await exec(`git fetch --depth ${depth} origin develop`); + await exec(`git fetch --depth ${depth} origin ${process.env.CIRCLE_BRANCH}`); + return true; + } catch (error: unknown) { + console.error(`Failed to fetch with depth ${depth}:`, error); + return false; + } +} + +/** + * Attempts to fetch the necessary commits until the merge base is found. + * It tries different fetch depths and performs a full fetch if needed. + * + * @throws If an unexpected error occurs during the execution of git commands. + */ +async function fetchUntilMergeBaseFound() { + const depths = [1, 10, 100]; + for (const depth of depths) { + console.log(`Attempting git diff with depth ${depth}...`); + await fetchWithDepth(depth); + + try { + await exec(`git merge-base origin/HEAD HEAD`); + return; + } catch (error: unknown) { + if ( + error instanceof Error && + hasProperty(error, 'code') && + error.code === 1 + ) { + console.error(`Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`); + } else { + throw error; + } + } + } + await exec(`git fetch --unshallow origin develop`); +} + +/** + * Performs a git diff command to get the list of files changed between the current branch and the origin. + * It first ensures that the necessary commits are fetched until the merge base is found. + * + * @returns The output of the git diff command, listing the changed files. + * @throws If unable to get the diff after fetching the merge base or if an unexpected error occurs. + */ +async function gitDiff(): Promise { + await fetchUntilMergeBaseFound(); + const { stdout: diffResult } = await exec(`git diff --name-only origin/HEAD...${process.env.CIRCLE_BRANCH}`); + if (!diffResult) { + throw new Error('Unable to get diff after full checkout.'); + } + return diffResult; +} + +/** + * Stores the output of git diff to a file. + * + * @returns Returns a promise that resolves when the git diff output is successfully stored. + */ +async function storeGitDiffOutput() { + try { + console.log("Attempting to get git diff..."); + const diffOutput = await gitDiff(); + console.log(diffOutput); + + // Create the directory + const outputDir = 'changed-files'; + fs.mkdirSync(outputDir, { recursive: true }); + + // Store the output of git diff + const outputPath = path.resolve(outputDir, 'changed-files.txt'); + fs.writeFileSync(outputPath, diffOutput); + + console.log(`Git diff results saved to ${outputPath}`); + process.exit(0); + } catch (error: any) { + console.error('An error occurred:', error.message); + process.exit(1); + } +} + +storeGitDiffOutput(); diff --git a/development/lib/retry.js b/development/lib/retry.js index e6e5dfc040af..813a63aa44e4 100644 --- a/development/lib/retry.js +++ b/development/lib/retry.js @@ -11,12 +11,12 @@ * @param {string} [args.rejectionMessage] - The message for the rejected promise * this function will return in the event of failure. (Default: "Retry limit * reached") - * @param {boolean} [args.retryUntilFailure] - Retries until the function fails. + * @param {boolean} [args.stopAfterOneFailure] - Retries until the function fails. * @param {Function} functionToRetry - The function that is run and tested for * failure. * @returns {Promise<* | null | Error>} a promise that either resolves with one of the following: * - If successful, resolves with the return value of functionToRetry. - * - If functionToRetry fails while retryUntilFailure is true, resolves with null. + * - If functionToRetry fails while stopAfterOneFailure is true, resolves with null. * - Otherwise it is rejected with rejectionMessage. */ async function retry( @@ -24,7 +24,7 @@ async function retry( retries, delay = 0, rejectionMessage = 'Retry limit reached', - retryUntilFailure = false, + stopAfterOneFailure = false, }, functionToRetry, ) { @@ -36,7 +36,7 @@ async function retry( try { const result = await functionToRetry(); - if (!retryUntilFailure) { + if (!stopAfterOneFailure) { return result; } } catch (error) { @@ -46,18 +46,22 @@ async function retry( console.error('error caught in retry():', error); } - if (attempts < retries) { - console.log('Ready to retry() again'); + if (stopAfterOneFailure) { + throw new Error('Test failed. No more retries will be performed'); } - if (retryUntilFailure) { - return null; + if (attempts < retries) { + console.log('Ready to retry() again'); } } finally { attempts += 1; } } + if (stopAfterOneFailure) { + return null; + } + throw new Error(rejectionMessage); } diff --git a/test/e2e/changedFilesUtil.js b/test/e2e/changedFilesUtil.js new file mode 100644 index 000000000000..5ead76203db0 --- /dev/null +++ b/test/e2e/changedFilesUtil.js @@ -0,0 +1,44 @@ +const fs = require('fs').promises; +const path = require('path'); + +const BASE_PATH = path.resolve(__dirname, '..', '..'); +const CHANGED_FILES_PATH = path.join( + BASE_PATH, + 'changed-files', + 'changed-files.txt', +); + +/** + * Reads the list of changed files from the git diff file. + * + * @returns {Promise} An array of changed file paths. + */ +async function readChangedFiles() { + try { + const data = await fs.readFile(CHANGED_FILES_PATH, 'utf8'); + const changedFiles = data.split('\n'); + return changedFiles; + } catch (error) { + console.error('Error reading from file:', error); + return ''; + } +} + +/** + * Filters the list of changed files to include only E2E test files within the 'test/e2e/' directory. + * + * @returns {Promise} An array of filtered E2E test file paths. + */ +async function filterE2eChangedFiles() { + const changedFiles = await readChangedFiles(); + const e2eChangedFiles = changedFiles + .filter( + (file) => + file.startsWith('test/e2e/') && + (file.endsWith('.spec.js') || file.endsWith('.spec.ts')), + ) + .map((file) => `${BASE_PATH}/${file}`); + return e2eChangedFiles; +} + +module.exports = { filterE2eChangedFiles }; diff --git a/test/e2e/run-all.js b/test/e2e/run-all.js index 0ff043261a7b..d52a37e9afe6 100644 --- a/test/e2e/run-all.js +++ b/test/e2e/run-all.js @@ -6,6 +6,7 @@ const { hideBin } = require('yargs/helpers'); const { runInShell } = require('../../development/lib/run-command'); const { exitWithError } = require('../../development/lib/exit-with-error'); const { loadBuildTypesConfig } = require('../../development/lib/build-type'); +const { filterE2eChangedFiles } = require('./changedFilesUtil'); // These tests should only be run on Flask for now. const FLASK_ONLY_TESTS = ['test-snap-namelookup.spec.js']; @@ -30,9 +31,47 @@ const getTestPathsForTestDir = async (testDir) => { return testPaths; }; +// Quality Gate Retries +const RETRIES_FOR_NEW_OR_CHANGED_TESTS = 5; + +/** + * Runs the quality gate logic to filter and append changed or new tests if present. + * + * @param {string} fullTestList - List of test paths to be considered. + * @param {string[]} changedOrNewTests - List of changed or new test paths. + * @returns {string} The updated full test list. + */ +async function applyQualityGate(fullTestList, changedOrNewTests) { + let qualityGatedList = fullTestList; + + if (changedOrNewTests.length > 0) { + // Filter to include only the paths present in fullTestList + const filteredTests = changedOrNewTests.filter((test) => + fullTestList.includes(test), + ); + + // If there are any filtered tests, append them to fullTestList + if (filteredTests.length > 0) { + const filteredTestsString = filteredTests.join('\n'); + for (let i = 0; i < RETRIES_FOR_NEW_OR_CHANGED_TESTS; i++) { + qualityGatedList += `\n${filteredTestsString}`; + } + } + } + + return qualityGatedList; +} + // For running E2Es in parallel in CI -function runningOnCircleCI(testPaths) { - const fullTestList = testPaths.join('\n'); +async function runningOnCircleCI(testPaths) { + const changedOrNewTests = await filterE2eChangedFiles(); + console.log('Changed or new test list:', changedOrNewTests); + + const fullTestList = await applyQualityGate( + testPaths.join('\n'), + changedOrNewTests, + ); + console.log('Full test list:', fullTestList); fs.writeFileSync('test/test-results/fullTestList.txt', fullTestList); @@ -46,7 +85,7 @@ function runningOnCircleCI(testPaths) { // Report if no tests found, exit gracefully if (result.indexOf('There were no tests found') !== -1) { console.log(`run-all.js info: Skipping this node because "${result}"`); - return []; + return { fullTestList: [] }; } // If there's no text file, it means this node has no tests, so exit gracefully @@ -54,13 +93,15 @@ function runningOnCircleCI(testPaths) { console.log( 'run-all.js info: Skipping this node because there is no myTestList.txt', ); - return []; + return { fullTestList: [] }; } // take the space-delimited result and split into an array - return fs + const myTestList = fs .readFileSync('test/test-results/myTestList.txt', { encoding: 'utf8' }) .split(' '); + + return { fullTestList: myTestList, changedOrNewTests }; } async function main() { @@ -204,8 +245,10 @@ async function main() { await fs.promises.mkdir('test/test-results/e2e', { recursive: true }); let myTestList; + let changedOrNewTests; if (process.env.CIRCLECI) { - myTestList = runningOnCircleCI(testPaths); + ({ fullTestList: myTestList, changedOrNewTests = [] } = + await runningOnCircleCI(testPaths)); } else { myTestList = testPaths; } @@ -217,7 +260,12 @@ async function main() { if (testPath !== '') { testPath = testPath.replace('\n', ''); // sometimes there's a newline at the end of the testPath console.log(`\nExecuting testPath: ${testPath}\n`); - await runInShell('node', [...args, testPath]); + + const isTestChangedOrNew = changedOrNewTests?.includes(testPath); + const qualityGateArg = isTestChangedOrNew + ? ['--stop-after-one-failure'] + : []; + await runInShell('node', [...args, ...qualityGateArg, testPath]); } } } diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js index a4c0496dbda6..0acf0e571cdb 100644 --- a/test/e2e/run-e2e-test.js +++ b/test/e2e/run-e2e-test.js @@ -35,7 +35,7 @@ async function main() { 'Set how many times the test should be retried upon failure.', type: 'number', }) - .option('retry-until-failure', { + .option('stop-after-one-failure', { default: false, description: 'Retries until the test fails', type: 'boolean', @@ -73,7 +73,7 @@ async function main() { mmi, e2eTestPath, retries, - retryUntilFailure, + stopAfterOneFailure, leaveRunning, updateSnapshot, updatePrivacySnapshot, @@ -141,7 +141,7 @@ async function main() { const dir = 'test/test-results/e2e'; fs.mkdir(dir, { recursive: true }); - await retry({ retries, retryUntilFailure }, async () => { + await retry({ retries, stopAfterOneFailure }, async () => { await runInShell('yarn', [ 'mocha', `--config=${configFile}`,