Skip to content

Commit

Permalink
feat: poc of consolidated asset view across chains. balances are mocked
Browse files Browse the repository at this point in the history
  • Loading branch information
gambinish committed Oct 25, 2024
1 parent 0d637fb commit c923dfe
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 75 deletions.
20 changes: 10 additions & 10 deletions ui/components/app/assets/token-cell/token-cell.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getTokenList } from '../../../../selectors';
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
import { getCurrentCurrency, getTokenList } from '../../../../selectors';
import { TokenListItem } from '../../../multichain';
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
import { useIsOriginalTokenSymbol } from '../../../../hooks/useIsOriginalTokenSymbol';
Expand All @@ -11,6 +10,7 @@ type TokenCellProps = {
address: string;
symbol: string;
string?: string;
tokenFiatAmount: number;
image: string;
onClick?: (arg: string) => void;
};
Expand All @@ -20,8 +20,10 @@ export default function TokenCell({
image,
symbol,
string,
tokenFiatAmount,
onClick,
}: TokenCellProps) {
const currentCurrency = useSelector(getCurrentCurrency);
const tokenList = useSelector(getTokenList);
const tokenData = Object.values(tokenList).find(
(token) =>
Expand All @@ -30,13 +32,11 @@ export default function TokenCell({
);
const title = tokenData?.name || symbol;
const tokenImage = tokenData?.iconUrl || image;
const formattedFiat = useTokenFiatAmount(address, string, symbol, {}, false);
const locale = useSelector(getIntlLocale);
const primary = new Intl.NumberFormat(locale, {
minimumSignificantDigits: 1,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
}).format(string.toString());
const formattedFiatBalance = new Intl.NumberFormat(locale, {
currency: currentCurrency.toUpperCase(),
style: 'currency',
}).format(tokenFiatAmount);

const isOriginalTokenSymbol = useIsOriginalTokenSymbol(address, symbol);

Expand All @@ -45,8 +45,8 @@ export default function TokenCell({
onClick={onClick ? () => onClick(address) : undefined}
tokenSymbol={symbol}
tokenImage={tokenImage}
primary={`${primary || 0}`}
secondary={isOriginalTokenSymbol ? formattedFiat : null}
primary={string}
secondary={formattedFiatBalance}
title={title}
isOriginalTokenSymbol={isOriginalTokenSymbol}
address={address}
Expand Down
115 changes: 50 additions & 65 deletions ui/components/app/assets/token-list/token-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,25 @@ import {
Display,
JustifyContent,
} from '../../../../helpers/constants/design-system';
import { TokenWithBalance } from '../asset-list/asset-list';
import { sortAssets } from '../util/sort';
import {
getAllTokens,
getCurrentChainId,
getCurrencyRates,
getPreferences,
getSelectedAccount,
getSelectedAccountTokenBalancesAcrossChains,
getSelectedAccountTokensAcrossChains,
getShouldHideZeroBalanceTokens,
getTokenExchangeRates,
getTokensMarketDataAcrossChains,
} from '../../../../selectors';
import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance';
import { getConversionRate } from '../../../../ducks/metamask/metamask';
import { useNativeTokenBalance } from '../asset-list/native-token/use-native-token-balance';
import { useTokenTracker } from '../../../../hooks/useTokenTracker';

type TokenListProps = {
onTokenClick: (arg: string) => void;
nativeToken: ReactNode;
};

function aggregateTokensByAccount(data: Record<string, any>) {
// Initialize an empty object to hold tokens by account
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tokensByAccount: Record<string, any> = {};

// Loop through each chain (0x1, 0x89, etc.)
for (const chainId in data) {
// Ensure we're only iterating over data's own properties
if (data[chainId]) {
// Loop through each account in the chain
const chainData = data[chainId];
for (const accountId in chainData) {
if (chainData[accountId]) {
// If the accountId does not exist in the tokensByAccount object, initialize it with an empty array
if (!tokensByAccount[accountId]) {
tokensByAccount[accountId] = [];
}

// Loop through each token associated with the account
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chainData[accountId].forEach((token: any) => {
// Add the chainId to each token object
const tokenWithChain = { ...token, chainId };

// Push the token to the respective account's token list
tokensByAccount[accountId].push(tokenWithChain);
});
}
}
}
}

return tokensByAccount;
}

export default function TokenList({
onTokenClick,
nativeToken,
Expand All @@ -80,44 +44,65 @@ export default function TokenList({
shallowEqual,
);

const allTokens = useSelector(getAllTokens);
const aggregatedCrossChainTokensByAccount =
aggregateTokensByAccount(allTokens);
const selectedAccountTokensChains: Record<string, any> = useSelector(
getSelectedAccountTokensAcrossChains,
);

const selectedAccountTokenBalancesAcrossChains: Record<string, any> =
useSelector(getSelectedAccountTokenBalancesAcrossChains);

const marketData = useSelector(getTokensMarketDataAcrossChains);
const currencyRates = useSelector(getCurrencyRates);

// Select token data, and token balances
// Consolidate both data structures into an array of tokens, along with their balances
// include an isNative boolean to indicate that these are _not_ native tokens
const consolidatedBalances = () => {
const tokensWithBalance: any[] = [];

console.log(aggregatedCrossChainTokensByAccount[selectedAccount.address]);
// Iterate over each chainId in accountTokensByChain
Object.keys(selectedAccountTokensChains).forEach((chainId: string) => {
// For each token in the chain, add the balance from tokenBalancesByChain
selectedAccountTokensChains[chainId].forEach(
(token: Record<string, any>) => {
const { address } = token;
const balance =
selectedAccountTokenBalancesAcrossChains[chainId]?.[address] ||
'0.00000'; // Default to "0.00000" if no balance found

const { tokensWithBalances: crossChainTokensWithBalances } = useTokenTracker({
tokens: aggregatedCrossChainTokensByAccount[selectedAccount.address],
address: selectedAccount?.address,
includeFailedTokens: true,
hideZeroBalanceTokens: shouldHideZeroBalanceTokens,
});
console.log('crossChainTokensWithBalances: ', crossChainTokensWithBalances);
const baseCurrency = marketData[chainId]?.[address]?.currency;

const tokenMarketPrice = marketData[chainId]?.[address]?.price || '0';
const tokenExchangeRate =
currencyRates[baseCurrency]?.conversionRate || '0';

// Add the token with its balance to the result array
tokensWithBalance.push({
...token,
balance,
tokenFiatAmount: tokenMarketPrice * tokenExchangeRate * balance,
isNative: false,
string: balance.toString(),
});
},
);
});

const { tokensWithBalances, loading } = useAccountTotalFiatBalance(
selectedAccount,
shouldHideZeroBalanceTokens,
) as {
tokensWithBalances: TokenWithBalance[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mergedRates: any;
loading: boolean;
return tokensWithBalance;
};

const sortedTokens = useMemo(() => {
// TODO filter assets by networkTokenFilter before sorting
return sortAssets(
[nativeTokenWithBalance, ...tokensWithBalances],
tokenSortConfig,
);
const consolidatedTokensWithBalances = consolidatedBalances(); // TODO include nativeTokens in this list
// .filter() // TODO filter assets by networkTokenFilter before sorting
return sortAssets(consolidatedTokensWithBalances, tokenSortConfig);
}, [
tokensWithBalances,
tokenSortConfig,
tokenNetworkFilter,
conversionRate,
contractExchangeRates,
]);

const loading = false; // TODO should we include loading/polling logic?
return loading ? (
<Box
display={Display.Flex}
Expand Down
97 changes: 97 additions & 0 deletions ui/selectors/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,95 @@ export function getMetaMaskCachedBalances(state) {
return {};
}

/**
* Based on the current account address, return the balance for the native token of all chain networks on that account
*
* @param {object} state - Redux state
* @returns {object} An object of tokens with balances for the given account. Data relationship will be chainId => balance
*/
export function getSelectedAccountNativeTokenCachedBalanceByChainId(state) {
const { accountsByChainId } = state.metamask;
const { address: selectedAddress } = getSelectedInternalAccount(state);

const balancesByChainId = {};
for (const [chainId, accounts] of Object.entries(accountsByChainId || {})) {
if (accounts[selectedAddress]) {
balancesByChainId[chainId] = accounts[selectedAddress].balance;
}
}
return balancesByChainId;
}

/**
* Based on the current account address, query for all tokens across all chain networks on that account.
* This will eventually be exposed in a new piece of state called `tokenBalances`, which will including the new polling mechanism to stay up to date
*
* @param {object} state - Redux state
* @returns {object} An array of tokens with balances for the given account. Data relationship will be chainId => balance
*/
export function getSelectedAccountTokensAcrossChains(state) {
const { allTokens } = state.metamask;
const { address: selectedAddress } = getSelectedInternalAccount(state);

// Initialize an empty object to hold tokens by chainId for the selected account
const tokensByChain = {};

// Loop through each chain (0x1, 0x89, etc.)
for (const chainId in allTokens) {
if (allTokens[chainId]) {
const chainData = allTokens[chainId];

// Check if the selected account exists within this chain
if (chainData[selectedAddress]) {
// Ensure the chainId key exists in tokensByChain
if (!tokensByChain[chainId]) {
tokensByChain[chainId] = [];
}

// Add each token to the array under its chainId
chainData[selectedAddress].forEach((token) => {
const tokenWithChain = { ...token, chainId };
tokensByChain[chainId].push(tokenWithChain);
});
}
}
}

return tokensByChain;
}

/**
* Based on the current account address, query for all tokens across all chain networks on that account.
* This will eventually be exposed in a new piece of state called `tokenBalances`, which will including the new polling mechanism to stay up to date
*
* @param {object} state - Redux state
* @returns {object} An array of tokens with balances for the given account. Data relationship will be chainId => balance
*/
export function getSelectedAccountTokenBalancesAcrossChains(state) {
const accountTokens = getSelectedAccountTokensAcrossChains(state);

// TODO: read this from tokenBalances state
function generateRandomBalance(min = 10, max = 20) {
const factor = 100000; // 10^5 to get 5 decimal places
const randomValue = Math.random() * (max - min) + min;
return Math.floor(randomValue * factor) / factor;
}

const tokenBalancesByChain = {};

Object.keys(accountTokens).forEach((chainId) => {
tokenBalancesByChain[chainId] = {};

accountTokens[chainId].forEach((token) => {
const { address } = token;

tokenBalancesByChain[chainId][address] = generateRandomBalance();
});
});

return tokenBalancesByChain;
}

/**
* @typedef {import('./selectors.types').InternalAccountWithBalance} InternalAccountWithBalance
*/
Expand Down Expand Up @@ -591,6 +680,10 @@ export const getTokensMarketData = (state) => {
return state.metamask.marketData?.[chainId];
};

export const getTokensMarketDataAcrossChains = (state) => {
return state.metamask.marketData;
};

export function getAddressBook(state) {
const chainId = getCurrentChainId(state);
if (!state.metamask.addressBook[chainId]) {
Expand Down Expand Up @@ -1244,6 +1337,10 @@ export function getUSDConversionRate(state) {
?.usdConversionRate;
}

export function getCurrencyRates(state) {
return state.metamask.currencyRates;
}

export function getWeb3ShimUsageStateForOrigin(state, origin) {
return state.metamask.web3ShimUsageOrigins[origin];
}
Expand Down

0 comments on commit c923dfe

Please sign in to comment.