diff --git a/app/components/UI/CollectibleContractElement/index.js b/app/components/UI/CollectibleContractElement/index.js index 742d69dcdcd..9d8cc6a51fd 100644 --- a/app/components/UI/CollectibleContractElement/index.js +++ b/app/components/UI/CollectibleContractElement/index.js @@ -12,7 +12,6 @@ import Engine from '../../../core/Engine'; import { removeFavoriteCollectible } from '../../../actions/collectibles'; import { collectibleContractsSelector } from '../../../reducers/collectibles'; import { useTheme } from '../../../util/theme'; -import { selectChainId } from '../../../selectors/networkController'; import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; import Icon, { IconName, @@ -315,7 +314,6 @@ CollectibleContractElement.propTypes = { const mapStateToProps = (state) => ({ collectibleContracts: collectibleContractsSelector(state), - chainId: selectChainId(state), selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), }); diff --git a/app/components/UI/CollectibleContracts/index.js b/app/components/UI/CollectibleContracts/index.js index 52ccc725c63..e3e4777cb29 100644 --- a/app/components/UI/CollectibleContracts/index.js +++ b/app/components/UI/CollectibleContracts/index.js @@ -29,10 +29,7 @@ import { compareTokenIds } from '../../../util/tokens'; import CollectibleDetectionModal from '../CollectibleDetectionModal'; import { useTheme } from '../../../util/theme'; import { MAINNET } from '../../../constants/network'; -import { - selectChainId, - selectProviderType, -} from '../../../selectors/networkController'; +import { selectProviderType } from '../../../selectors/networkController'; import { selectDisplayNftMedia, selectIsIpfsGatewayEnabled, @@ -43,6 +40,7 @@ import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletV import { useMetrics } from '../../../components/hooks/useMetrics'; import { RefreshTestId, SpinnerTestId } from './constants'; import { debounce } from 'lodash'; +import { useChainId } from '../../../selectors/hooks'; const createStyles = (colors) => StyleSheet.create({ @@ -100,7 +98,6 @@ const debouncedNavigation = debounce((navigation, collectible) => { */ const CollectibleContracts = ({ selectedAddress, - chainId, networkType, navigation, collectibleContracts, @@ -131,6 +128,8 @@ const CollectibleContracts = ({ [navigation], ); + const chainId = useChainId(); + /** * Method that checks if the collectible is inside the collectibles array. If it is not it means the * collectible has been ignored, hence we should not call the updateMetadata which executes the addNft fct @@ -262,10 +261,11 @@ const CollectibleContracts = ({ key={item.address} contractCollectibles={contractCollectibles} collectiblesVisible={index === 0} + chainId={chainId} /> ); }, - [collectibles, onItemPress], + [collectibles, onItemPress, chainId], ); const renderFavoriteCollectibles = useCallback(() => { @@ -388,10 +388,6 @@ CollectibleContracts.propTypes = { * Network type */ networkType: PropTypes.string, - /** - * Chain id - */ - chainId: PropTypes.string, /** * Selected address */ @@ -438,7 +434,6 @@ CollectibleContracts.propTypes = { const mapStateToProps = (state) => ({ networkType: selectProviderType(state), - chainId: selectChainId(state), selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), useNftDetection: selectUseNftDetection(state), collectibleContracts: collectibleContractsSelector(state), @@ -450,8 +445,9 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = (dispatch) => ({ - removeFavoriteCollectible: (selectedAddress, chainId, collectible) => - dispatch(removeFavoriteCollectible(selectedAddress, chainId, collectible)), + removeFavoriteCollectible: (selectedAddress, chainId, collectible) => { + dispatch(removeFavoriteCollectible(selectedAddress, chainId, collectible)); + }, }); export default connect( diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx index 7ab0ad470d5..e9cc9228fc6 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx @@ -10,7 +10,6 @@ import { useTheme } from '../../../../../util/theme'; import { TOKEN_RATE_UNDEFINED } from '../../constants'; import { deriveBalanceFromAssetMarketDetails } from '../../util/deriveBalanceFromAssetMarketDetails'; import { - selectChainId, selectProviderConfig, selectTicker, } from '../../../../../selectors/networkController'; @@ -54,6 +53,7 @@ interface TokenListItemProps { showScamWarningModal: boolean; showRemoveMenu: (arg: TokenI) => void; setShowScamWarningModal: (arg: boolean) => void; + chainId: string; } export const TokenListItem = ({ @@ -61,13 +61,13 @@ export const TokenListItem = ({ showScamWarningModal, showRemoveMenu, setShowScamWarningModal, + chainId, }: TokenListItemProps) => { const navigation = useNavigation(); const { colors } = useTheme(); const { data: tokenBalances } = useTokenBalancesController(); const { type } = useSelector(selectProviderConfig); - const chainId = useSelector(selectChainId); const ticker = useSelector(selectTicker); const isOriginalNativeTokenSymbol = useIsOriginalNativeTokenSymbol( chainId, @@ -95,7 +95,7 @@ export const TokenListItem = ({ currentCurrency, ); - const pricePercentChange1d = itemAddress + const pricePercentChange1d = itemAddress?.startsWith('0x') ? tokenExchangeRates?.[itemAddress as `0x${string}`]?.pricePercentChange1d : tokenExchangeRates?.[zeroAddress() as Hex]?.pricePercentChange1d; @@ -158,10 +158,12 @@ export const TokenListItem = ({ if (isLineaMainnet) return images['LINEA-MAINNET']; - if (CustomNetworkImgMapping[chainId]) { + const isValidChainId = (id: string): id is `0x${string}` => + /^0x[0-9a-fA-F]+$/.test(id); + + if (isValidChainId(chainId) && CustomNetworkImgMapping[chainId]) { return CustomNetworkImgMapping[chainId]; } - return ticker ? images[ticker] : undefined; }; diff --git a/app/components/UI/Tokens/TokenList/index.tsx b/app/components/UI/Tokens/TokenList/index.tsx index f5460d51281..c4cfb083d2e 100644 --- a/app/components/UI/Tokens/TokenList/index.tsx +++ b/app/components/UI/Tokens/TokenList/index.tsx @@ -9,7 +9,6 @@ import { } from '../../../../components/hooks/useMetrics'; import { useTheme } from '../../../../util/theme'; import { createDetectedTokensNavDetails } from '../../../Views/DetectedTokens'; -import { selectChainId } from '../../../../selectors/networkController'; import { selectDetectedTokens } from '../../../../selectors/tokensController'; import { getDecimalChainId } from '../../../../util/networks'; import createStyles from '../styles'; @@ -18,6 +17,7 @@ import { TokenI } from '../types'; import { strings } from '../../../../../locales/i18n'; import { TokenListFooter } from './TokenListFooter'; import { TokenListItem } from './TokenListItem'; +import { useChainId } from '../../../../selectors/hooks'; interface TokenListProps { tokens: TokenI[]; @@ -44,7 +44,7 @@ export const TokenList = ({ const { colors } = useTheme(); const { trackEvent } = useMetrics(); - const chainId = useSelector(selectChainId); + const chainId = useChainId(); const detectedTokens = useSelector(selectDetectedTokens); const [showScamWarningModal, setShowScamWarningModal] = useState(false); @@ -80,6 +80,7 @@ export const TokenList = ({ renderItem={({ item }) => ( = ({ tokens }) => { const { trackEvent } = useMetrics(); const { data: tokenBalances } = useTokenBalancesController(); - const chainId = useSelector(selectChainId); + const chainId = useChainId(); const networkClientId = useSelector(selectNetworkClientId); const hideZeroBalanceTokens = useSelector( (state: RootState) => state.settings.hideZeroBalanceTokens, diff --git a/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx index bf787491664..60fd03a7919 100644 --- a/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx +++ b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx @@ -22,9 +22,9 @@ import { useNavigation } from '@react-navigation/native'; import Engine from '../../../core/Engine'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { selectChainId } from '../../../selectors/networkController'; import { useSelector } from 'react-redux'; import { selectDisplayNftMedia } from '../../../selectors/preferencesController'; +import { useChainId } from '../../../selectors/hooks'; const walletImage = require('../../../images/wallet-alpha.png'); @@ -32,7 +32,7 @@ const NFTAutoDetectionModal = () => { const { styles } = useStyles(styleSheet, {}); const sheetRef = useRef(null); const navigation = useNavigation(); - const chainId = useSelector(selectChainId); + const chainId = useChainId(); const displayNftMedia = useSelector(selectDisplayNftMedia); const { trackEvent } = useMetrics(); const enableNftDetectionAndDismissModal = useCallback( diff --git a/app/components/Views/NftDetails/NftDetails.tsx b/app/components/Views/NftDetails/NftDetails.tsx index 452a61c5ab0..7a1b159792e 100644 --- a/app/components/Views/NftDetails/NftDetails.tsx +++ b/app/components/Views/NftDetails/NftDetails.tsx @@ -26,10 +26,7 @@ import ClipboardManager from '../../../core/ClipboardManager'; import { useDispatch, useSelector } from 'react-redux'; import { showAlert } from '../../../actions/alert'; import { strings } from '../../../../locales/i18n'; -import { - selectChainId, - selectTicker, -} from '../../../selectors/networkController'; +import { selectTicker } from '../../../selectors/networkController'; import etherscanLink from '@metamask/etherscan-link'; import { selectConversionRate, @@ -47,11 +44,12 @@ import { renderShortText } from '../../../util/general'; import { prefixUrlWithProtocol } from '../../../util/browser'; import { formatTimestampToYYYYMMDD } from '../../../util/date'; import MAX_TOKEN_ID_LENGTH from './nftDetails.utils'; +import { useChainId } from '../../../selectors/hooks'; const NftDetails = () => { const navigation = useNavigation(); const { collectible } = useParams(); - const chainId = useSelector(selectChainId); + const chainId = useChainId(); const dispatch = useDispatch(); const currentCurrency = useSelector(selectCurrentCurrency); const ticker = useSelector(selectTicker); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index a5ccb6e25ea..6b019fb362e 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -105,6 +105,8 @@ import { ButtonVariants } from '../../../component-library/components/Buttons/Bu import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance'; import { isObject } from 'lodash'; +import { useChainId } from '../../../selectors/hooks'; + const createStyles = ({ colors, typography }: Theme) => StyleSheet.create({ base: { @@ -178,6 +180,10 @@ const Wallet = ({ const dispatch = useDispatch(); const networkConfigurations = useSelector(selectNetworkConfigurations); + /** + * Selected chain id (with potential support for per-dapp selected chain) + */ + const chainId = useChainId(); /** * Object containing the balance of the current selected account */ @@ -215,7 +221,7 @@ const Wallet = ({ * Provider configuration for the current selected network */ const providerConfig = useSelector(selectProviderConfig); - const prevChainId = usePrevious(providerConfig.chainId); + const prevChainId = usePrevious(chainId); const isDataCollectionForMarketingEnabled = useSelector( (state: RootState) => state.security.dataCollectionForMarketing, @@ -324,9 +330,9 @@ const Wallet = ({ screen: Routes.SHEET.NETWORK_SELECTOR, }); trackEvent(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED, { - chain_id: getDecimalChainId(providerConfig.chainId), + chain_id: getDecimalChainId(chainId), }); - }, [navigate, providerConfig.chainId, trackEvent]); + }, [navigate, chainId, trackEvent]); const isNetworkDuplicated = Object.values(networkConfigurations).some( (networkConfiguration) => @@ -336,7 +342,7 @@ const Wallet = ({ ); const checkNftAutoDetectionModal = useCallback(() => { - const isOnMainnet = isMainNet(providerConfig.chainId); + const isOnMainnet = isMainNet(chainId); if (!useNftDetection && isOnMainnet && !isNFTAutoDetectionModalViewed) { navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { screen: Routes.MODAL.NFT_AUTO_DETECTION_MODAL, @@ -347,7 +353,7 @@ const Wallet = ({ dispatch, isNFTAutoDetectionModalViewed, navigation, - providerConfig.chainId, + chainId, useNftDetection, ]); @@ -380,14 +386,11 @@ const Wallet = ({ */ useEffect(() => { const networkOnboarded = getIsNetworkOnboarded( - providerConfig.chainId, + chainId, networkOnboardingState, ); - if ( - wizardStep > 0 || - (!networkOnboarded && prevChainId !== providerConfig.chainId) - ) { + if (wizardStep > 0 || (!networkOnboarded && prevChainId !== chainId)) { // Do not check since it will conflict with the onboarding wizard and/or network onboarding return; } @@ -401,7 +404,7 @@ const Wallet = ({ ).isZero(); const shouldShowStxOptInModal = await shouldShowSmartTransactionsOptInModal( - providerConfig.chainId, + chainId, providerConfig.rpcUrl, accountHasZeroBalance, ); @@ -424,7 +427,7 @@ const Wallet = ({ }, [ wizardStep, navigation, - providerConfig.chainId, + chainId, providerConfig.rpcUrl, networkOnboardingState, prevChainId, @@ -440,7 +443,7 @@ const Wallet = ({ }); }, /* eslint-disable-next-line */ - [navigation, providerConfig.chainId], + [navigation, chainId], ); useLayoutEffect(() => { diff --git a/app/selectors/hooks.test.ts b/app/selectors/hooks.test.ts new file mode 100644 index 00000000000..a91b37efbec --- /dev/null +++ b/app/selectors/hooks.test.ts @@ -0,0 +1,48 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; +import { useChainId } from './hooks'; +import { selectChainId } from '../selectors/networkController'; + +// Mock useSelector from react-redux +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +// Mock the selector +jest.mock('../selectors/networkController', () => ({ + selectChainId: jest.fn(), +})); + +describe('useChainId hook', () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test to avoid test contamination + }); + + it('should return the chain ID from the redux store', () => { + const mockChainId = 1; + + // Mock the selector to return the mockChainId + (useSelector as jest.Mock).mockImplementation((selectorFn) => { + if (selectorFn === selectChainId) { + return mockChainId; + } + }); + + const { result } = renderHook(() => useChainId()); + + expect(result.current).toBe(mockChainId); + }); + + it('should return undefined if the chain ID is not set', () => { + // Mock the selector to return undefined + (useSelector as jest.Mock).mockImplementation((selectorFn) => { + if (selectorFn === selectChainId) { + return undefined; + } + }); + + const { result } = renderHook(() => useChainId()); + + expect(result.current).toBeUndefined(); + }); +}); diff --git a/app/selectors/hooks.ts b/app/selectors/hooks.ts new file mode 100644 index 00000000000..0d2e56e64e7 --- /dev/null +++ b/app/selectors/hooks.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; +import { selectChainId } from '../selectors/networkController'; + +export const useChainId = () => { + // For now, just use the global chain ID from the selector + const chainId = useSelector(selectChainId); + + // Future implementation could enhance this to support per-dapp chain ID + return chainId; +};