From 2bca81a99f79c83788b5332ca737c74da2f6dc55 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Mon, 20 May 2024 15:24:39 +0200 Subject: [PATCH 1/3] Rename some components for consistency --- src/app/components/Account/index.tsx | 10 ++++++++-- ...ntDetailsCard.tsx => RuntimeAccountDetailsCard.tsx} | 8 ++++---- ...ntDetailsView.tsx => RuntimeAccountDetailsView.tsx} | 6 +++--- src/app/pages/RuntimeAccountDetailsPage/index.tsx | 4 ++-- src/app/pages/SearchResultsPage/SearchResultsList.tsx | 6 +++--- 5 files changed, 20 insertions(+), 14 deletions(-) rename src/app/pages/RuntimeAccountDetailsPage/{AccountDetailsCard.tsx => RuntimeAccountDetailsCard.tsx} (80%) rename src/app/pages/RuntimeAccountDetailsPage/{AccountDetailsView.tsx => RuntimeAccountDetailsView.tsx} (85%) diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx index 3ab89c2ba..2c05d6ab6 100644 --- a/src/app/components/Account/index.tsx +++ b/src/app/components/Account/index.tsx @@ -24,7 +24,7 @@ import { calculateFiatValue } from '../Balance/hooks' import { FiatMoneyAmount } from '../Balance/FiatMoneyAmount' import { getFiatCurrencyForScope, getTokensForScope, showFiatValues } from '../../../config' -type AccountProps = { +type RuntimeAccountDataProps = { account?: RuntimeAccount token?: EvmToken isLoading: boolean @@ -32,7 +32,13 @@ type AccountProps = { showLayer?: boolean } -export const Account: FC = ({ account, token, isLoading, tokenPrices, showLayer }) => { +export const RuntimeAccountData: FC = ({ + account, + token, + isLoading, + tokenPrices, + showLayer, +}) => { const { t } = useTranslation() const { isMobile } = useScreenSize() const address = account ? account.address_eth ?? account.address : undefined diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/RuntimeAccountDetailsCard.tsx similarity index 80% rename from src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx rename to src/app/pages/RuntimeAccountDetailsPage/RuntimeAccountDetailsCard.tsx index 18c57866d..a06321270 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx +++ b/src/app/pages/RuntimeAccountDetailsPage/RuntimeAccountDetailsCard.tsx @@ -2,10 +2,10 @@ import { FC } from 'react' import { SubPageCard } from '../../components/SubPageCard' import { useTranslation } from 'react-i18next' import { EvmToken, RuntimeAccount } from '../../../oasis-nexus/api' -import { AccountDetailsView } from './AccountDetailsView' +import { RuntimeAccountDetailsView } from './RuntimeAccountDetailsView' import { AllTokenPrices } from '../../../coin-gecko/api' -type AccountDetailsProps = { +type RuntimeAccountDetailsProps = { isLoading: boolean isError: boolean isContract: boolean @@ -14,7 +14,7 @@ type AccountDetailsProps = { tokenPrices: AllTokenPrices } -export const AccountDetailsCard: FC = ({ +export const RuntimeAccountDetailsCard: FC = ({ isLoading, isError, isContract, @@ -30,7 +30,7 @@ export const AccountDetailsCard: FC = ({ title={isContract ? t('contract.title') : t('account.title')} mainTitle > - ) : ( - { return ( - item.resultType === 'account')} resultComponent={item => ( - item.resultType === 'contract')} resultComponent={item => ( - Date: Mon, 20 May 2024 16:38:09 +0200 Subject: [PATCH 2/3] Refactor code for displaying consensus account details No real functionality change here, just moving components into separate files, and reducing parallel code paths. --- src/app/components/Account/index.tsx | 86 ++++++++++++++++++- .../ConsensusAccountDetailsCard.tsx | 76 +--------------- .../ConsensusAccountDetailsView.tsx | 29 +++++++ .../ConsensusAccountDetailsPage/index.tsx | 57 +----------- src/app/pages/ConsensusAccountsPage/index.tsx | 2 +- src/locales/en/translation.json | 1 + 6 files changed, 119 insertions(+), 132 deletions(-) create mode 100644 src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsView.tsx diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx index 2c05d6ab6..c874540eb 100644 --- a/src/app/components/Account/index.tsx +++ b/src/app/components/Account/index.tsx @@ -5,7 +5,7 @@ import { useScreenSize } from '../../hooks/useScreensize' import { StyledDescriptionList, StyledListTitleWithAvatar } from '../../components/StyledDescriptionList' import { CopyToClipboard } from '../../components/CopyToClipboard' import { TextSkeleton } from '../../components/Skeleton' -import { EvmToken, type RuntimeAccount } from '../../../oasis-nexus/api' +import { Account, EvmToken, type RuntimeAccount } from '../../../oasis-nexus/api' import { TokenPills } from './TokenPills' import { AccountLink } from './AccountLink' import { RouteUtils } from '../../utils/route-utils' @@ -23,6 +23,9 @@ import { RuntimeBalanceDisplay } from '../Balance/RuntimeBalanceDisplay' import { calculateFiatValue } from '../Balance/hooks' import { FiatMoneyAmount } from '../Balance/FiatMoneyAmount' import { getFiatCurrencyForScope, getTokensForScope, showFiatValues } from '../../../config' +import Box from '@mui/material/Box' +import { AccountSizeBadge } from '../AccountSizeBadge' +import { StyledListTitle } from '../../pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsCard' type RuntimeAccountDataProps = { account?: RuntimeAccount @@ -166,3 +169,84 @@ export const RuntimeAccountData: FC = ({ ) } + +export type ConsensusAccountDataProps = { + account?: Account + isLoading?: boolean + showLayer?: boolean + standalone?: boolean +} + +export const ConsensusAccountData: FC = ({ + account, + isLoading, + showLayer, + standalone, +}) => { + const { t } = useTranslation() + const { isMobile } = useScreenSize() + + if (!account || isLoading) return + + return ( + + {showLayer && ( + <> +
{t('common.layer')}
+
+ +
+ + )} + + + + + + +
+ + +
+
+ {t('account.totalBalance')} +
+
+ + {t('common.valueInToken', { + ...getPreciseNumberFormat(account.total), + ticker: account.ticker, + })} + +
+ {t('account.available')} +
+ {t('common.valueInToken', { + ...getPreciseNumberFormat(account.available), + ticker: account.ticker, + })} +
+ {t('common.staking')} +
+ {t('common.valueInToken', { + ...getPreciseNumberFormat(account.delegations_balance!), + ticker: account.ticker, + })} +
+ {t('account.debonding')} +
+ {t('common.valueInToken', { + ...getPreciseNumberFormat(account.debonding_delegations_balance!), + ticker: account.ticker, + })} +
+
{t('account.lastNonce')}
+
{account.nonce}
+
{t('account.birth')}
+
+ {/* TODO: provide date when it is implemented in the API */} + <>- +
+
+ ) +} diff --git a/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsCard.tsx b/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsCard.tsx index c2e7a8f7b..a571c27e0 100644 --- a/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsCard.tsx +++ b/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsCard.tsx @@ -1,18 +1,9 @@ import { FC } from 'react' import { styled } from '@mui/material/styles' import { useTranslation } from 'react-i18next' -import Box from '@mui/material/Box' import { Account } from '../../../oasis-nexus/api' -import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' -import { useScreenSize } from '../../hooks/useScreensize' -import { StyledDescriptionList, StyledListTitleWithAvatar } from '../../components/StyledDescriptionList' -import { AccountLink } from '../../components/Account/AccountLink' -import { AccountSizeBadge } from '../../components/AccountSizeBadge' -import { TextSkeleton } from '../../components/Skeleton' import { SubPageCard } from '../../components/SubPageCard' -import { CopyToClipboard } from '../../components/CopyToClipboard' -import { AccountAvatar } from '../../components/AccountAvatar' -import { CardEmptyState } from '../../components/CardEmptyState' +import { ConsensusAccountDetailsView } from './ConsensusAccountDetailsView' export const StyledListTitle = styled('dt')(({ theme }) => ({ marginLeft: theme.spacing(4), @@ -33,70 +24,7 @@ export const ConsensusAccountDetailsCard: FC = return ( - + ) } - -const ConsensusAccountDetails: FC = ({ account, isError, isLoading }) => { - const { t } = useTranslation() - const { isMobile } = useScreenSize() - - if (isLoading) return - if (isError) return - if (!account) return null - - return ( - - - - - - - -
- - -
-
- {t('account.totalBalance')} -
-
- - {t('common.valueInToken', { - ...getPreciseNumberFormat(account.total), - ticker: account.ticker, - })} - -
- {t('account.available')} -
- {t('common.valueInToken', { - ...getPreciseNumberFormat(account.available), - ticker: account.ticker, - })} -
- {t('common.staking')} -
- {t('common.valueInToken', { - ...getPreciseNumberFormat(account.delegations_balance!), - ticker: account.ticker, - })} -
- {t('account.debonding')} -
- {t('common.valueInToken', { - ...getPreciseNumberFormat(account.debonding_delegations_balance!), - ticker: account.ticker, - })} -
-
{t('account.lastNonce')}
-
{account.nonce}
-
{t('account.birth')}
-
- {/* TODO: provide date when it is implemented in the API */} - <>- -
-
- ) -} diff --git a/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsView.tsx b/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsView.tsx new file mode 100644 index 000000000..d6767350b --- /dev/null +++ b/src/app/pages/ConsensusAccountDetailsPage/ConsensusAccountDetailsView.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { CardEmptyState } from '../../components/CardEmptyState' +import { ConsensusAccountData, ConsensusAccountDataProps } from '../../components/Account' + +type ConsensusAccountDetailsViewProps = ConsensusAccountDataProps & { + isError?: boolean | undefined +} + +export const ConsensusAccountDetailsView: FC = ({ + account, + isError, + isLoading, + showLayer, + standalone, +}) => { + const { t } = useTranslation() + + if (isError || !account) return + + return ( + + ) +} diff --git a/src/app/pages/ConsensusAccountDetailsPage/index.tsx b/src/app/pages/ConsensusAccountDetailsPage/index.tsx index 4f20ba366..3bf4ca875 100644 --- a/src/app/pages/ConsensusAccountDetailsPage/index.tsx +++ b/src/app/pages/ConsensusAccountDetailsPage/index.tsx @@ -2,14 +2,9 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import { useHref, useLoaderData } from 'react-router-dom' import Grid from '@mui/material/Grid' -import { Account, useGetConsensusAccountsAddress } from '../../../oasis-nexus/api' +import { useGetConsensusAccountsAddress } from '../../../oasis-nexus/api' import { useScreenSize } from '../../hooks/useScreensize' -import { StyledDescriptionList } from '../../components/StyledDescriptionList' import { PageLayout } from '../../components/PageLayout' -import { TextSkeleton } from '../../components/Skeleton' -import { AccountSizeBadge } from '../../components/AccountSizeBadge' -import { AccountLink } from '../../components/Account/AccountLink' -import { RoundedBalance } from '../..//components/RoundedBalance' import { AddressLoaderData } from '../../utils/route-utils' import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { ConsensusAccountDetailsCard } from './ConsensusAccountDetailsCard' @@ -45,53 +40,3 @@ export const ConsensusAccountDetailsPage: FC = () => {
) } - -export const ConsensusAccountDetailsView: FC<{ - isLoading?: boolean - account: Account | undefined - standalone?: boolean -}> = ({ account, isLoading, standalone = false }) => { - const { t } = useTranslation() - const { isMobile } = useScreenSize() - - if (isLoading) return - if (!account) return null - - return ( - - {/* TODO: provide missing props when API is ready */} -
{t('common.size')}
-
- -
-
{t('common.address')}
-
- , -
-
{t('account.birth')}
-
- <>- -
-
{t('account.available')}
-
- -
-
{t('common.staked')}
-
- <>- -
-
{t('account.debonding')}
-
- <>- -
-
- {t('account.totalBalance')} -
-
- - - -
-
- ) -} diff --git a/src/app/pages/ConsensusAccountsPage/index.tsx b/src/app/pages/ConsensusAccountsPage/index.tsx index 5df98c75c..a6c825133 100644 --- a/src/app/pages/ConsensusAccountsPage/index.tsx +++ b/src/app/pages/ConsensusAccountsPage/index.tsx @@ -14,7 +14,7 @@ import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { CardHeaderWithCounter } from '../../components/CardHeaderWithCounter' import { VerticalList } from '../../components/VerticalList' import { AccountList } from 'app/components/AccountList' -import { ConsensusAccountDetailsView } from '../ConsensusAccountDetailsPage' +import { ConsensusAccountDetailsView } from '../ConsensusAccountDetailsPage/ConsensusAccountDetailsView' export const ConsensusAccountsPage: FC = () => { const [tableView, setTableView] = useState(TableLayout.Horizontal) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 1ff5e0ce7..14ee0d887 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -90,6 +90,7 @@ "height": "Height", "hide": "Hide", "invalidVotes": "Invalid votes", + "layer": "Layer", "loadMore": "Load more", "lessThanAmount": "< {{value, number}} ", "method": "Method", From b47b4f945bdf7ec0ef1c5326247ea0f44feab25c Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Mon, 20 May 2024 16:59:08 +0200 Subject: [PATCH 3/3] Support searching for consensus accounts --- .changelog/1422.trivial.md | 1 + .../SearchResultsPage/SearchResultsList.tsx | 31 +++++++++++++------ src/app/pages/SearchResultsPage/hooks.ts | 31 ++++++++++++++++--- .../useRedirectIfSingleResult.ts | 3 +- src/oasis-nexus/api.ts | 30 +++++++++++++----- 5 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 .changelog/1422.trivial.md diff --git a/.changelog/1422.trivial.md b/.changelog/1422.trivial.md new file mode 100644 index 000000000..73749ddf1 --- /dev/null +++ b/.changelog/1422.trivial.md @@ -0,0 +1 @@ +Support searching for consensus accounts (by address) diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 214a81653..792c3f02f 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -5,6 +5,7 @@ import { RuntimeBlockDetailView } from '../RuntimeBlockDetailPage' import { RouteUtils } from '../../utils/route-utils' import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage' import { RuntimeAccountDetailsView } from '../RuntimeAccountDetailsPage/RuntimeAccountDetailsView' +import { ConsensusAccountDetailsView } from '../ConsensusAccountDetailsPage/ConsensusAccountDetailsView' import { AccountResult, BlockResult, @@ -21,6 +22,7 @@ import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' import { TokenDetails } from '../../components/Tokens/TokenDetails' import { ProposalDetailView } from '../ProposalDetailsPage' +import { Account, Layer, RuntimeAccount } from '../../../oasis-nexus/api' /** * Component for displaying a list of search results @@ -81,16 +83,25 @@ export const SearchResultsList: FC<{ item.resultType === 'account')} - resultComponent={item => ( - - )} - link={acc => RouteUtils.getAccountRoute(acc, acc.address_eth ?? acc.address)} + resultComponent={item => + item.layer === Layer.consensus ? ( + + ) : ( + + ) + } + link={acc => RouteUtils.getAccountRoute(acc, (acc as RuntimeAccount).address_eth ?? acc.address)} linkLabel={t('search.results.accounts.viewLink')} /> diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts index 395a96cbf..cd63fe89a 100644 --- a/src/app/pages/SearchResultsPage/hooks.ts +++ b/src/app/pages/SearchResultsPage/hooks.ts @@ -15,6 +15,8 @@ import { useGetRuntimeBlockByHash, Proposal, useGetConsensusProposalsByName, + Account, + useGetConsensusAccountsAddress, } from '../../../oasis-nexus/api' import { RouteUtils } from '../../utils/route-utils' import { SearchParams } from '../../components/Search/search-utils' @@ -34,7 +36,7 @@ export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'b export type TransactionResult = SearchResultItemCore & RuntimeTransaction & { resultType: 'transaction' } -export type AccountResult = SearchResultItemCore & RuntimeAccount & { resultType: 'account' } +export type AccountResult = SearchResultItemCore & (RuntimeAccount | Account) & { resultType: 'account' } export type ContractResult = SearchResultItemCore & RuntimeAccount & { resultType: 'contract' } @@ -125,6 +127,7 @@ export function useTransactionsConditionally( results: queries.flatMap(query => query.data?.data.transactions).filter(isDefined), } } + export function useRuntimeAccountConditionally( currentScope: SearchScope | undefined, address: string | undefined, @@ -147,6 +150,23 @@ export function useRuntimeAccountConditionally( } } +export function useConsensusAccountConditionally(address: string | undefined): ConditionalResults { + const queries = RouteUtils.getEnabledNetworksForLayer(Layer.consensus).map(network => + // See explanation above + // eslint-disable-next-line react-hooks/rules-of-hooks + useGetConsensusAccountsAddress(network, address!, { + query: { + enabled: !!address, + }, + }), + ) + + return { + isLoading: queries.some(query => query.isInitialLoading), + results: queries.map(query => query.data?.data).filter(isDefined), + } +} + export function useRuntimeTokenConditionally( currentScope: SearchScope | undefined, nameFragment: string | undefined, @@ -198,7 +218,8 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams blockHeight: useBlocksByHeightConditionally(currentScope, q.blockHeight), blockHash: useBlocksByHashConditionally(currentScope, q.blockHash), txHash: useTransactionsConditionally(currentScope, q.txHash), - oasisAccount: useRuntimeAccountConditionally(currentScope, q.consensusAccount), + oasisConsensusAccount: useConsensusAccountConditionally(q.consensusAccount), + oasisRuntimeAccount: useRuntimeAccountConditionally(currentScope, q.consensusAccount), // TODO: remove evmBech32Account and use evmAccount when API is ready evmBech32Account: useRuntimeAccountConditionally(currentScope, q.evmBech32Account), tokens: useRuntimeTokenConditionally(currentScope, q.evmTokenNameFragment), @@ -208,7 +229,8 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams const blocks = [...queries.blockHeight.results, ...queries.blockHash.results] const transactions = queries.txHash.results || [] const accounts = [ - ...(queries.oasisAccount.results || []), + ...(queries.oasisConsensusAccount.results || []), + ...(queries.oasisRuntimeAccount.results || []), ...(queries.evmBech32Account.results || []), ].filter(isAccountNonEmpty) const tokens = queries.tokens.results @@ -223,9 +245,10 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams ...blocks.map((block): BlockResult => ({ ...block, resultType: 'block' })), ...transactions.map((tx): TransactionResult => ({ ...tx, resultType: 'transaction' })), ...accounts - .filter(account => !account.evm_contract) + .filter(account => !(account as RuntimeAccount).evm_contract) .map((account): AccountResult => ({ ...account, resultType: 'account' })), ...accounts + .filter((account): account is RuntimeAccount => account.layer !== Layer.consensus) .filter(account => account.evm_contract) .map((account): ContractResult => ({ ...account, resultType: 'contract' })), ...tokens.map((token): TokenResult => ({ ...token, resultType: 'token' })), diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts index ebcb816b8..3c21caffe 100644 --- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts +++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts @@ -5,6 +5,7 @@ import { RouteUtils } from '../../utils/route-utils' import { isItemInScope, SearchScope } from '../../../types/searchScope' import { Network } from '../../../types/network' import { exhaustedTypeWarning } from '../../../types/errors' +import { RuntimeAccount } from '../../../oasis-nexus/api' /** If search only finds one result then redirect to it */ export function useRedirectIfSingleResult( @@ -33,7 +34,7 @@ export function useRedirectIfSingleResult( redirectTo = RouteUtils.getTransactionRoute(item, item.eth_hash || item.hash) break case 'account': - redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) + redirectTo = RouteUtils.getAccountRoute(item, (item as RuntimeAccount).address_eth ?? item.address) break case 'contract': redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index 4d406b7b1..406364940 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -5,6 +5,7 @@ import { consensusDecimals, getTokensForScope, paraTimesConfig } from '../config import * as generated from './generated/api' import { QueryKey, UseQueryOptions, UseQueryResult, useQuery } from '@tanstack/react-query' import { + Account, EvmToken, EvmTokenType, GetRuntimeAccountsAddress, @@ -107,15 +108,30 @@ declare module './generated/api' { } } -export const isAccountEmpty = (account: RuntimeAccount) => { - const { balances, evm_balances, stats } = account - const { total_received, total_sent, num_txns } = stats - const hasNoBalances = [...balances, ...evm_balances].every(b => b.balance === '0' || b.balance === '0.0') - const hasNoTransactions = total_received === '0' && total_sent === '0' && num_txns === 0 - return hasNoBalances && hasNoTransactions +export const isAccountEmpty = (account: RuntimeAccount | Account) => { + if (account.layer === Layer.consensus) { + const { available, size, nonce, debonding_delegations_balance, delegations_balance, escrow, total } = + account as Account + return ( + available === '0' && + debonding_delegations_balance === '0' && + delegations_balance === '0' && + escrow === '0' && + total === '0' && + nonce === 0 && + size === 'XXS' + ) + // TODO: we should also check the number of transactions, where it becomes available + } else { + const { balances, evm_balances, stats } = account as RuntimeAccount + const { total_received, total_sent, num_txns } = stats + const hasNoBalances = [...balances, ...evm_balances].every(b => b.balance === '0' || b.balance === '0.0') + const hasNoTransactions = total_received === '0' && total_sent === '0' && num_txns === 0 + return hasNoBalances && hasNoTransactions + } } -export const isAccountNonEmpty = (account: RuntimeAccount) => !isAccountEmpty(account) +export const isAccountNonEmpty = (account: RuntimeAccount | Account) => !isAccountEmpty(account) export const groupAccountTokenBalances = (account: Omit): RuntimeAccount => { const tokenBalances: Partial> = {}