diff --git a/ui/components/app/assets/asset-list/asset-list.test.tsx b/ui/components/app/assets/asset-list/asset-list.test.tsx index 00a47df1c633..6b6dbbee88ad 100644 --- a/ui/components/app/assets/asset-list/asset-list.test.tsx +++ b/ui/components/app/assets/asset-list/asset-list.test.tsx @@ -72,6 +72,18 @@ jest.mock('../../../../store/actions', () => { })), tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), tokenBalancesStopPollingByPollingToken: jest.fn(), + addImportedTokens: jest.fn(), + }; +}); + +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: () => mockDispatch, }; }); diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 19d9ecdd4c0d..5ac7a84e0b29 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -1,14 +1,17 @@ -import React, { useContext, useState } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useContext, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Token } from '@metamask/assets-controllers'; +import { NetworkConfiguration } from '@metamask/network-controller'; import TokenList from '../token-list'; import { PRIMARY } from '../../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { getAllDetectedTokensForSelectedAddress, getDetectedTokensInCurrentNetwork, - getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getIsTokenNetworkFilterEqualCurrentNetwork, getSelectedAccount, + getSelectedAddress, + getUseTokenDetection, } from '../../../../selectors'; import { getMultichainIsEvm, @@ -23,9 +26,10 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, + MetaMetricsTokenEventSource, } from '../../../../../shared/constants/metametrics'; import DetectedToken from '../../detected-token/detected-token'; -import { DetectedTokensBanner, ReceiveModal } from '../../../multichain'; +import { ReceiveModal } from '../../../multichain'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -35,6 +39,16 @@ import { } from '../../../multichain/ramps-card/ramps-card'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF +import { + getCurrentChainId, + getNetworkConfigurationsByChainId, + getSelectedNetworkClientId, +} from '../../../../../shared/modules/selectors/networks'; +import { addImportedTokens } from '../../../../store/actions'; +import { + AssetType, + TokenStandard, +} from '../../../../../shared/constants/transaction'; import AssetListControlBar from './asset-list-control-bar'; import NativeToken from './native-token'; @@ -54,6 +68,7 @@ export type AssetListProps = { }; const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { + const dispatch = useDispatch(); const [showDetectedTokens, setShowDetectedTokens] = useState(false); const selectedAccount = useSelector(getSelectedAccount); const t = useI18nContext(); @@ -74,14 +89,19 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { }); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; - const isTokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( - getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - ); const isTokenNetworkFilterEqualCurrentNetwork = useSelector( getIsTokenNetworkFilterEqualCurrentNetwork, ); + const allNetworks: Record<`0x${string}`, NetworkConfiguration> = useSelector( + getNetworkConfigurationsByChainId, + ); + const networkClientId = useSelector(getSelectedNetworkClientId); + const selectedAddress = useSelector(getSelectedAddress); + const useTokenDetection = useSelector(getUseTokenDetection); + const currentChainId = useSelector(getCurrentChainId); + const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); const [showReceiveModal, setShowReceiveModal] = useState(false); @@ -104,32 +124,88 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; - const detectedTokensMultichain = useSelector( - getAllDetectedTokensForSelectedAddress, - ); + const detectedTokensMultichain: { + [key: `0x${string}`]: Token[]; + } = useSelector(getAllDetectedTokensForSelectedAddress); + + const multichainDetectedTokensLength = Object.values( + detectedTokensMultichain || {}, + ).reduce((acc, tokens) => acc + tokens.length, 0); + + // Add detected tokens to sate + useEffect(() => { + const importAllDetectedTokens = async () => { + // If autodetect tokens toggle is OFF, return + if (!useTokenDetection) { + return; + } + // TODO add event for MetaMetricsEventName.TokenAdded + + if ( + process.env.PORTFOLIO_VIEW && + !isTokenNetworkFilterEqualCurrentNetwork + ) { + const importPromises = Object.entries(detectedTokensMultichain).map( + async ([networkId, tokens]) => { + const chainConfig = allNetworks[networkId as `0x${string}`]; + const { defaultRpcEndpointIndex } = chainConfig; + const { networkClientId: networkInstanceId } = + chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; + + await dispatch( + addImportedTokens(tokens as Token[], networkInstanceId), + ); + tokens.forEach((importedToken) => { + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: chainConfig.chainId, + }, + }); + }); + }, + ); + + await Promise.all(importPromises); + } else if (detectedTokens.length > 0) { + await dispatch(addImportedTokens(detectedTokens, networkClientId)); + detectedTokens.forEach((importedToken: Token) => { + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: currentChainId, + }, + }); + }); + } + }; + importAllDetectedTokens(); + }, [ + isTokenNetworkFilterEqualCurrentNetwork, + selectedAddress, + networkClientId, + detectedTokens.length, + multichainDetectedTokensLength, + ]); - const totalTokens = - process.env.PORTFOLIO_VIEW && - !isTokenNetworkFilterEqualCurrentNetwork && - detectedTokensMultichain - ? (Object.values(detectedTokensMultichain).reduce( - // @ts-expect-error TS18046: 'tokenArray' is of type 'unknown' - (count, tokenArray) => count + tokenArray.length, - 0, - ) as number) - : detectedTokens.length; return ( <> - {totalTokens && - totalTokens > 0 && - !isTokenDetectionInactiveOnNonMainnetSupportedNetwork ? ( - setShowDetectedTokens(true)} - margin={4} - marginBottom={1} - /> - ) : null} ({ setTokenNetworkFilter: jest.fn(), updateSlides: jest.fn(), removeSlide: jest.fn(), + addImportedTokens: jest.fn(), })); // Mock the dispatch function diff --git a/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx b/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx index df7c2ef56d69..68a7978ffb53 100644 --- a/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx @@ -15,6 +15,7 @@ jest.mock('../../../store/actions', () => ({ setTokenNetworkFilter: jest.fn(), updateSlides: jest.fn(), removeSlide: jest.fn(), + addImportedTokens: jest.fn(), })); // Mock the dispatch function