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),
+);