diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 8dba73d50b5b..11394c661547 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -77,6 +77,7 @@ import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountT import { useGetFormattedTokensPerChain } from '../../../hooks/useGetFormattedTokensPerChain'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { AggregatedBalance } from '../../ui/aggregated-balance/aggregated-balance'; import WalletOverview from './wallet-overview'; import CoinButtons from './coin-buttons'; import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; @@ -99,6 +100,92 @@ export type CoinOverviewProps = { isSigningEnabled: boolean; }; +export const LegacyAggregatedBalance = ({ + classPrefix, + account, + balance, + balanceIsCached, + handleSensitiveToggle, +}: { + classPrefix: string; + account: InternalAccount; + balance: string; + balanceIsCached: boolean; + handleSensitiveToggle: () => void; +}) => { + const isTokenNetworkFilterEqualCurrentNetwork = useSelector( + getIsTokenNetworkFilterEqualCurrentNetwork, + ); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const allChainIDs = useSelector(getChainIdsToPoll) as string[]; + const shouldShowFiat = useMultichainSelector( + getMultichainShouldShowFiat, + account, + ); + const { privacyMode, showNativeTokenAsMainBalance } = + useSelector(getPreferences); + const isTestnet = useSelector(getIsTestnet); + + const { formattedTokensWithBalancesPerChain } = useGetFormattedTokensPerChain( + account, + shouldHideZeroBalanceTokens, + isTokenNetworkFilterEqualCurrentNetwork, + allChainIDs, + ); + + const { totalFiatBalance } = useAccountTotalCrossChainFiatBalance( + account, + formattedTokensWithBalancesPerChain, + ); + + const isNotAggregatedFiatBalance = + !shouldShowFiat || showNativeTokenAsMainBalance || isTestnet; + + let balanceToDisplay; + if (isNotAggregatedFiatBalance) { + balanceToDisplay = balance; + } else { + balanceToDisplay = totalFiatBalance; + } + + if (!balanceToDisplay) { + return ; + } + return ( + <> + + + + ); +}; + export const CoinOverview = ({ account, balance, @@ -149,36 +236,7 @@ export const CoinOverview = ({ getIsTokenNetworkFilterEqualCurrentNetwork, ); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); - const allChainIDs = useSelector(getChainIdsToPoll); - const { formattedTokensWithBalancesPerChain } = useGetFormattedTokensPerChain( - account, - shouldHideZeroBalanceTokens, - isTokenNetworkFilterEqualCurrentNetwork, - allChainIDs, - ); - const { totalFiatBalance } = useAccountTotalCrossChainFiatBalance( - account, - formattedTokensWithBalancesPerChain, - ); - - const shouldShowFiat = useMultichainSelector( - getMultichainShouldShowFiat, - account, - ); - const isEvm = useSelector(getMultichainIsEvm); - const isNotAggregatedFiatBalance = - !shouldShowFiat || showNativeTokenAsMainBalance || isTestnet || !isEvm; - - let balanceToDisplay; - if (isNotAggregatedFiatBalance) { - balanceToDisplay = balance; - } else { - balanceToDisplay = totalFiatBalance; - } const tokensMarketData = useSelector(getTokensMarketData); const [isOpen, setIsOpen] = useState(true); @@ -296,44 +354,20 @@ export const CoinOverview = ({ onMouseEnter={handleMouseEnter} ref={setBoxRef} > - {balanceToDisplay ? ( - <> - - - + {isEvm ? ( + ) : ( - + )} {balanceIsCached && ( diff --git a/ui/components/app/wallet-overview/non-evm-overview.test.tsx b/ui/components/app/wallet-overview/non-evm-overview.test.tsx index b6adc187ad60..38db5fd4dc57 100644 --- a/ui/components/app/wallet-overview/non-evm-overview.test.tsx +++ b/ui/components/app/wallet-overview/non-evm-overview.test.tsx @@ -86,6 +86,9 @@ const mockBuyableChainsWithBtc = [...mockBuyableChainsWithoutBtc, mockBtcChain]; const mockMetamaskStore = { ...mockState.metamask, + accountsAssets: { + [mockNonEvmAccount.id]: [MultichainNativeAssets.BITCOIN], + }, internalAccounts: { accounts: { [mockNonEvmAccount.id]: mockNonEvmAccount, @@ -103,7 +106,7 @@ const mockMetamaskStore = { }, // (Multichain) RatesController fiatCurrency: 'usd', - rates: { + conversionRates: { [Cryptocurrency.Btc]: { conversionRate: '1.000', conversionDate: 0, @@ -125,6 +128,9 @@ const mockRampsStore = { function getStore(state?: Record) { return configureMockStore([thunk])({ metamask: mockMetamaskStore, + localeMessages: { + currentLocale: 'en', + }, ramps: mockRampsStore, ...state, }); @@ -178,7 +184,9 @@ describe('NonEvmOverview', () => { preferences: { showNativeTokenAsMainBalance: false, tokenNetworkFilter: {}, + privacyMode: false, }, + currentCurrency: 'usd', conversionRates: { [MultichainNativeAssets.BITCOIN]: { rate: '1', @@ -201,6 +209,9 @@ describe('NonEvmOverview', () => { ...mockMetamaskStore, // The balances won't be available balances: {}, + accountsAssets: { + [mockNonEvmAccount.id]: [], + }, }, }), ); diff --git a/ui/components/ui/aggregated-balance/aggregated-balance.tsx b/ui/components/ui/aggregated-balance/aggregated-balance.tsx new file mode 100644 index 000000000000..e5708c8477a8 --- /dev/null +++ b/ui/components/ui/aggregated-balance/aggregated-balance.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import classnames from 'classnames'; +import { + AlignItems, + Display, + FlexWrap, + IconColor, + JustifyContent, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { + Box, + ButtonIcon, + ButtonIconSize, + IconName, + SensitiveText, +} from '../../component-library'; +import { + getCurrentCurrency, + getTokenBalances, +} from '../../../ducks/metamask/metamask'; +import { + getAccountAssets, + getMultichainAggregatedBalance, + getMultichainNativeTokenBalance, +} from '../../../selectors/assets'; +import { getPreferences, getSelectedInternalAccount } from '../../../selectors'; +import { formatWithThreshold } from '../../app/assets/util/formatWithThreshold'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import Spinner from '../spinner'; + +export const AggregatedBalance = ({ + classPrefix, + balanceIsCached, + handleSensitiveToggle, +}: { + classPrefix: string; + balanceIsCached: boolean; + handleSensitiveToggle: () => void; +}) => { + const { privacyMode, showNativeTokenAsMainBalance } = + useSelector(getPreferences); + const locale = useSelector(getIntlLocale); + const balances = useSelector(getTokenBalances); + const assets = useSelector(getAccountAssets); + const selectedAccount = useSelector(getSelectedInternalAccount); + const currentCurrency = useSelector(getCurrentCurrency); + const multichainAggregatedBalance = useSelector((state) => + getMultichainAggregatedBalance(state, selectedAccount), + ); + const multichainNativeTokenBalance = useSelector((state) => + getMultichainNativeTokenBalance(state, selectedAccount), + ); + + const formattedFiatDisplay = formatWithThreshold( + multichainAggregatedBalance, + 0.01, + locale, + { + style: 'currency', + currency: currentCurrency.toUpperCase(), + }, + ); + + const formattedTokenDisplay = formatWithThreshold( + multichainNativeTokenBalance.amount, + 0.00001, + locale, + { + minimumFractionDigits: 5, + maximumFractionDigits: 5, + }, + ).replace(/(\.0+|(?<=\.\d+)0+)$/u, ''); // strip trailing zeros + + if (!balances || !assets[selectedAccount.id]?.length) { + return ; + } + + return ( + <> + + + {showNativeTokenAsMainBalance + ? formattedTokenDisplay + : formattedFiatDisplay} + + + {showNativeTokenAsMainBalance + ? multichainNativeTokenBalance.unit + : currentCurrency.toUpperCase()} + + + + + + ); +}; diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index efd823bfde76..9fea5bada827 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -277,6 +277,9 @@ describe('toast display', () => { }, selectedAccount: selectedAccountId ?? mockAccount.id, }, + accountsAssets: { + [selectedAccountId ?? mockAccount.id]: [], + }, subjects: { [mockOrigin]: { permissions: { diff --git a/ui/selectors/assets.ts b/ui/selectors/assets.ts index 896cb1643d92..66f2acf53c15 100644 --- a/ui/selectors/assets.ts +++ b/ui/selectors/assets.ts @@ -20,7 +20,7 @@ import { getPreferences, getTokensAcrossChainsByAccountAddressSelector, } from './selectors'; -import { getMultichainBalances } from './multichain'; +import { getMultichainBalances, getMultichainNetwork } from './multichain'; export type AssetsState = { metamask: MultichainAssetsControllerState; @@ -200,3 +200,67 @@ export const getMultiChainAssets = createDeepEqualSelector( }); }, ); + +const zeroBalanceAssetFallback = { amount: 0, unit: '' }; + +export const getMultichainAggregatedBalance = createDeepEqualSelector( + (_state, selectedAccount) => selectedAccount, + getMultichainNetwork, + getMultichainBalances, + getAccountAssets, + getAssetsRates, + ( + selectedAccountAddress, + currentNetwork, + multichainBalances, + accountAssets, + assetRates, + ) => { + const assetIds = accountAssets?.[selectedAccountAddress.id] || []; + const balances = multichainBalances?.[selectedAccountAddress.id]; + + let aggregatedBalance = new BigNumber(0); + + assetIds.forEach((assetId: CaipAssetId) => { + const { chainId } = parseCaipAssetType(assetId); + if (chainId === currentNetwork.chainId) { + const balance = balances?.[assetId] || zeroBalanceAssetFallback; + const rate = assetRates?.[assetId]?.rate || '0'; + const balanceInFiat = new BigNumber(balance.amount).times(rate); + + aggregatedBalance = aggregatedBalance.plus(balanceInFiat); + } + }); + + return aggregatedBalance.toNumber(); + }, +); + +export const getMultichainNativeTokenBalance = createDeepEqualSelector( + (_state, selectedAccount) => selectedAccount, + getMultichainNetwork, + getMultichainBalances, + getAccountAssets, + ( + selectedAccountAddress, + currentNetwork, + multichainBalances, + accountAssets, + ) => { + const assetIds = accountAssets?.[selectedAccountAddress.id] || []; + const balances = multichainBalances?.[selectedAccountAddress.id]; + + let nativeTokenBalance = zeroBalanceAssetFallback; + + assetIds.forEach((assetId: CaipAssetId) => { + const { chainId, assetNamespace } = parseCaipAssetType(assetId); + if (chainId === currentNetwork.chainId && assetNamespace === 'slip44') { + const balance = balances?.[assetId] || zeroBalanceAssetFallback; + + nativeTokenBalance = balance; + } + }); + + return nativeTokenBalance; + }, +); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index b174d7645636..93a742b54279 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -489,3 +489,9 @@ export function getMultichainConversionRate( ? getConversionRate(state) : conversionRate; } + +export const getMultichainConversionRateSelector = createSelector( + (state) => state, + (_state, account) => account, + (state, account) => getMultichainConversionRate(state, account), +);