From 9b42569a4a2097fb5ade019d05c3bd62979777d0 Mon Sep 17 00:00:00 2001 From: vinnyhoward Date: Sun, 17 Nov 2024 22:24:02 -0700 Subject: [PATCH] feat: updated asset details to show correct info in charts for both native and non-native tokens --- .../UI/AssetOverview/AssetOverview.tsx | 126 ++++++++++++------ .../UI/AssetOverview/Balance/Balance.tsx | 112 ++++++++++++++-- .../Tokens/TokenList/TokenListItem/index.tsx | 4 +- app/components/UI/Tokens/types.ts | 4 +- app/selectors/currencyRateController.ts | 10 -- app/selectors/multichain.ts | 10 +- 6 files changed, 197 insertions(+), 69 deletions(-) diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx index 995d61584c0..4e0c4cbadaa 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -1,7 +1,7 @@ -import { zeroAddress } from 'ethereumjs-util'; import React, { useCallback, useEffect } from 'react'; import { TouchableOpacity, View } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; import { strings } from '../../../../locales/i18n'; import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/TokenOverview.selectors'; import { newAssetTransaction } from '../../../actions/transaction'; @@ -14,8 +14,12 @@ import { import { selectConversionRate, selectCurrentCurrency, + selectCurrencyRates, } from '../../../selectors/currencyRateController'; -import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; +import { + selectContractExchangeRates, + selectTokenMarketData, +} from '../../../selectors/tokenRatesController'; import { selectAccountsByChainId } from '../../../selectors/accountTrackerController'; import { selectContractBalances } from '../../../selectors/tokenBalancesController'; import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; @@ -53,6 +57,8 @@ import { createBuyNavigationDetails } from '../Ramp/routes/utils'; import { TokenI } from '../Tokens/types'; import AssetDetailsActions from '../../../components/Views/AssetDetails/AssetDetailsActions'; +const isPortfolioViewEnabled = process.env.PORTFOLIO_VIEW === 'true'; + interface AssetOverviewProps { navigation: { navigate: (route: string, params: Record) => void; @@ -69,8 +75,9 @@ const AssetOverview: React.FC = ({ displaySwapsButton, }: AssetOverviewProps) => { const [timePeriod, setTimePeriod] = React.useState('1d'); - const currentCurrency = useSelector(selectCurrentCurrency); const conversionRate = useSelector(selectConversionRate); + const conversionRateByTicker = useSelector(selectCurrencyRates); + const currentCurrency = useSelector(selectCurrentCurrency); const accountsByChainId = useSelector(selectAccountsByChainId); const primaryCurrency = useSelector( (state: RootState) => state.settings.primaryCurrency, @@ -81,12 +88,20 @@ const AssetOverview: React.FC = ({ ); const { trackEvent } = useMetrics(); const tokenExchangeRates = useSelector(selectContractExchangeRates); + const tokenExchangeRateByChainId = useSelector(selectTokenMarketData); const tokenBalances = useSelector(selectContractBalances); - const chainId = useSelector((state: RootState) => selectChainId(state)); - const ticker = useSelector((state: RootState) => selectTicker(state)); + const selectedChainId = useSelector((state: RootState) => + selectChainId(state), + ); + const selectedTicker = useSelector((state: RootState) => selectTicker(state)); + + const chainId = isPortfolioViewEnabled + ? (asset.chainId as Hex) + : selectedChainId; + const ticker = isPortfolioViewEnabled ? asset.symbol : selectedTicker; const { data: prices = [], isLoading } = useTokenHistoricalPrices({ - address: asset.isETH ? zeroAddress() : asset.address, + address: asset.address, chainId, timePeriod, vsCurrency: currentCurrency, @@ -201,55 +216,84 @@ const AssetOverview: React.FC = ({ ); const itemAddress = safeToChecksumAddress(asset.address); - const exchangeRate = itemAddress - ? tokenExchangeRates?.[itemAddress]?.price - : undefined; + + let exchangeRate; + if (!isPortfolioViewEnabled) { + exchangeRate = itemAddress + ? tokenExchangeRates?.[itemAddress]?.price + : undefined; + } else { + exchangeRate = + tokenExchangeRateByChainId?.[chainId]?.[itemAddress as Hex]?.price; + } let balance, balanceFiat; - if (asset.isETH) { - balance = renderFromWei( - //@ts-expect-error - This should be fixed at the accountsController selector level, ongoing discussion - accountsByChainId[toHexadecimal(chainId)][selectedAddress]?.balance, - ); - balanceFiat = weiToFiat( - hexToBN( + if (!isPortfolioViewEnabled) { + if (asset.isETH) { + balance = renderFromWei( //@ts-expect-error - This should be fixed at the accountsController selector level, ongoing discussion accountsByChainId[toHexadecimal(chainId)][selectedAddress]?.balance, - ), - conversionRate, - currentCurrency, - ); + ); + balanceFiat = weiToFiat( + hexToBN( + //@ts-expect-error - This should be fixed at the accountsController selector level, ongoing discussion + accountsByChainId[toHexadecimal(chainId)][selectedAddress]?.balance, + ), + conversionRate, + currentCurrency, + ); + } else { + balance = + itemAddress && tokenBalances?.[itemAddress] + ? renderFromTokenMinimalUnit( + tokenBalances[itemAddress], + asset.decimals, + ) + : 0; + balanceFiat = balanceToFiat( + balance, + conversionRateByTicker[asset.symbol].conversionRate, + exchangeRate, + currentCurrency, + ); + } } else { - balance = - itemAddress && tokenBalances?.[itemAddress] - ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], asset.decimals) - : 0; - balanceFiat = balanceToFiat( - balance, - conversionRate, - exchangeRate, - currentCurrency, - ); + balance = asset.balance; + balanceFiat = asset.balanceFiat; } let mainBalance, secondaryBalance; - if (primaryCurrency === 'ETH') { - mainBalance = `${balance} ${asset.symbol}`; - secondaryBalance = balanceFiat; + if (!isPortfolioViewEnabled) { + if (primaryCurrency === 'ETH') { + mainBalance = `${balance} ${asset.symbol}`; + secondaryBalance = balanceFiat; + } else { + mainBalance = !balanceFiat ? `${balance} ${asset.symbol}` : balanceFiat; + secondaryBalance = !balanceFiat + ? balanceFiat + : `${balance} ${asset.symbol}`; + } } else { - mainBalance = !balanceFiat ? `${balance} ${asset.symbol}` : balanceFiat; - secondaryBalance = !balanceFiat - ? balanceFiat - : `${balance} ${asset.symbol}`; + mainBalance = `${balance} ${asset.symbol}`; + secondaryBalance = asset.balanceFiat; } let currentPrice = 0; let priceDiff = 0; - if (asset.isETH) { - currentPrice = conversionRate || 0; - } else if (exchangeRate && conversionRate) { - currentPrice = exchangeRate * conversionRate; + if (!isPortfolioViewEnabled) { + if (asset.isETH) { + currentPrice = conversionRate || 0; + } else if (exchangeRate && conversionRate) { + currentPrice = exchangeRate * conversionRate; + } + } else { + const tickerConversionRate = + conversionRateByTicker[asset.symbol].conversionRate; + currentPrice = + exchangeRate && tickerConversionRate + ? exchangeRate * tickerConversionRate + : 0; } const comparePrice = prices[0]?.[1] || 0; diff --git a/app/components/UI/AssetOverview/Balance/Balance.tsx b/app/components/UI/AssetOverview/Balance/Balance.tsx index fed53bd539a..fe81320c418 100644 --- a/app/components/UI/AssetOverview/Balance/Balance.tsx +++ b/app/components/UI/AssetOverview/Balance/Balance.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { View } from 'react-native'; +import { Hex } from '@metamask/utils'; import { strings } from '../../../../../locales/i18n'; import { useStyles } from '../../../../component-library/hooks'; import styleSheet from './Balance.styles'; @@ -9,6 +10,7 @@ import { selectNetworkName } from '../../../../selectors/networkInfos'; import { selectChainId } from '../../../../selectors/networkController'; import { getTestNetImageByChainId, + getDefaultNetworkByChainId, isLineaMainnetByChainId, isMainnetByChainId, isTestNet, @@ -20,6 +22,7 @@ import Badge from '../../../../component-library/components/Badges/Badge/Badge'; import NetworkMainAssetLogo from '../../NetworkMainAssetLogo'; import AvatarToken from '../../../../component-library/components/Avatars/Avatar/variants/AvatarToken'; import { AvatarSize } from '../../../../component-library/components/Avatars/Avatar'; +import NetworkAssetLogo from '../../NetworkAssetLogo'; import Text, { TextVariant, } from '../../../../component-library/components/Texts/Text'; @@ -27,6 +30,11 @@ import { TokenI } from '../../Tokens/types'; import { useNavigation } from '@react-navigation/native'; import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; import StakingBalance from '../../Stake/components/StakingBalance/StakingBalance'; +import { + PopularList, + UnpopularNetworkList, + CustomNetworkImgMapping, +} from '../../../../util/networks/customNetworks'; interface BalanceProps { asset: TokenI; @@ -34,6 +42,8 @@ interface BalanceProps { secondaryBalance?: string; } +const isPortfolioViewEnabled = process.env.PORTFOLIO_VIEW === 'true'; + export const NetworkBadgeSource = (chainId: string, ticker: string) => { const isMainnet = isMainnetByChainId(chainId); const isLineaMainnet = isLineaMainnetByChainId(chainId); @@ -51,7 +61,93 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { const { styles } = useStyles(styleSheet, {}); const navigation = useNavigation(); const networkName = useSelector(selectNetworkName); - const chainId = useSelector(selectChainId); + const selectedChainId = useSelector(selectChainId); + + const chainId = isPortfolioViewEnabled + ? (asset.chainId as Hex) + : selectedChainId; + + const isMainnet = isMainnetByChainId(chainId); + const isLineaMainnet = isLineaMainnetByChainId(chainId); + const ticker = asset.symbol; + + const renderNetworkAvatar = useCallback(() => { + if (!isPortfolioViewEnabled && asset.isETH) { + return ; + } + + if (isPortfolioViewEnabled && asset.isNative) { + return ( + + ); + } + + return ( + + ); + }, [ + asset.isETH, + asset.image, + asset.symbol, + asset.isNative, + asset.chainId, + styles.ethLogo, + ]); + + const networkBadgeSource = useCallback( + (currentChainId: Hex) => { + if (!isPortfolioViewEnabled) { + if (isTestNet(chainId)) return getTestNetImageByChainId(chainId); + if (isMainnet) return images.ETHEREUM; + + if (isLineaMainnet) return images['LINEA-MAINNET']; + + if (CustomNetworkImgMapping[chainId]) { + return CustomNetworkImgMapping[chainId]; + } + + return ticker ? images[ticker as keyof typeof images] : undefined; + } + if (isTestNet(currentChainId)) + return getTestNetImageByChainId(currentChainId); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const defaultNetwork = getDefaultNetworkByChainId(currentChainId) as any; + + if (defaultNetwork) { + return defaultNetwork.imageSource; + } + + const unpopularNetwork = UnpopularNetworkList.find( + (networkConfig) => networkConfig.chainId === currentChainId, + ); + + const customNetworkImg = CustomNetworkImgMapping[currentChainId]; + + const popularNetwork = PopularList.find( + (networkConfig) => networkConfig.chainId === currentChainId, + ); + + const network = unpopularNetwork || popularNetwork; + if (network) { + return network.rpcPrefs.imageSource; + } + if (customNetworkImg) { + return customNetworkImg; + } + }, + [chainId, isLineaMainnet, isMainnet, ticker], + ); return ( @@ -69,20 +165,12 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { badgeElement={ } > - {asset.isETH ? ( - - ) : ( - - )} + {renderNetworkAvatar()} {asset.name || asset.symbol} diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx index 45bd70fa4cd..bbdfc332877 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx @@ -175,13 +175,13 @@ export const TokenListItem = ({ mainBalance = balanceValueFormatted; secondaryBalance = strings('wallet.unable_to_find_conversion_rate'); } + + asset = { ...asset, balanceFiat }; } else { mainBalance = asset.balance; secondaryBalance = asset.balanceFiat; } - asset = { ...asset, balanceFiat }; - const isMainnet = isMainnetByChainId(chainId); const isLineaMainnet = isLineaMainnetByChainId(chainId); diff --git a/app/components/UI/Tokens/types.ts b/app/components/UI/Tokens/types.ts index e3fcfb89c8a..90fa5ad8b49 100644 --- a/app/components/UI/Tokens/types.ts +++ b/app/components/UI/Tokens/types.ts @@ -23,6 +23,6 @@ export interface TokenI { hasBalanceError?: boolean; isStaked?: boolean | undefined; nativeAsset?: TokenI | undefined; - chainId?: string; // TODO: may need to remove optional - isNative?: boolean; // TODO: may need to remove optional + chainId?: string; + isNative?: boolean; } diff --git a/app/selectors/currencyRateController.ts b/app/selectors/currencyRateController.ts index e998444c059..92fd7ba4e08 100644 --- a/app/selectors/currencyRateController.ts +++ b/app/selectors/currencyRateController.ts @@ -56,13 +56,3 @@ export const selectConversionRateByTicker = createSelector( 0 : 0, ); - -export const selectConversionRateByTickerAlt = createSelector( - selectCurrencyRateControllerState, - (_: RootState, ticker: string) => ticker, - (currencyRateControllerState: CurrencyRateState, ticker: string) => - ticker - ? currencyRateControllerState?.currencyRates?.[ticker]?.conversionRate || - 0 - : 0, -); diff --git a/app/selectors/multichain.ts b/app/selectors/multichain.ts index 7e8ef2bf65a..3a15bbb063a 100644 --- a/app/selectors/multichain.ts +++ b/app/selectors/multichain.ts @@ -1,5 +1,6 @@ import { createSelector } from 'reselect'; import { Hex } from '@metamask/utils'; +import { zeroAddress } from 'ethereumjs-util'; import { RootState } from '../reducers'; import I18n from '../../locales/i18n'; import { @@ -167,6 +168,7 @@ export const selectAccountTokensAcrossChains = createSelector( logo: token.image, isETH: false, isNative: false, + symbol: getTicker(ticker), }; }); @@ -204,10 +206,14 @@ export const selectAccountTokensAcrossChains = createSelector( style: 'currency', }).format(tokenFiatAmount); } - tokensByChain[chainId].push({ ...nativeTokenInfo, - address: '', + // TODO: Check if this is correct for native tokens + // or do we need the wrapped native token address? + // for example the AVAX token has + // 0x0000000000000000000000000000000000000000 on its network + // but the wrapped version is 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7 + address: zeroAddress() as Hex, balance: nativeBalanceFormatted, balanceFiat: nativeBalanceFiat, chainId,