diff --git a/.changelog/1160.trivial.md b/.changelog/1160.trivial.md new file mode 100644 index 000000000..b681e9a47 --- /dev/null +++ b/.changelog/1160.trivial.md @@ -0,0 +1 @@ +Consensus dashboard account list card diff --git a/src/app/components/AccountList/index.tsx b/src/app/components/AccountList/index.tsx new file mode 100644 index 000000000..1a90893ea --- /dev/null +++ b/src/app/components/AccountList/index.tsx @@ -0,0 +1,82 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' +import { Table, TableCellAlign, TableColProps } from '../../components/Table' +import { RoundedBalance } from '../../components/RoundedBalance' +import { Account } from '../../../oasis-nexus/api' +import { TablePaginationProps } from '../Table/TablePagination' +import { AccountLink } from '../Account/AccountLink' +import { AccountSizeBadge } from '../AccountSizeBadge' + +type AccountListProps = { + accounts?: Account[] + isLoading: boolean + limit: number + pagination: false | TablePaginationProps +} + +export const AccountList: FC = ({ isLoading, limit, pagination, accounts }) => { + const { t } = useTranslation() + const tableColumns: TableColProps[] = [ + { align: TableCellAlign.Center, key: 'size', content: t('common.size') }, + { key: 'address', content: t('common.address') }, + { align: TableCellAlign.Right, key: 'available', content: t('account.available') }, + { align: TableCellAlign.Right, key: 'staked', content: t('account.staked') }, + { align: TableCellAlign.Right, key: 'debonding', content: t('account.debonding') }, + { align: TableCellAlign.Right, key: 'total', content: t('account.totalBalance') }, + ] + const tableRows = accounts?.map(account => ({ + key: account.address, + data: [ + { + content: ( + + + + ), + key: 'size', + }, + { + content: , + key: 'address', + }, + { + align: TableCellAlign.Right, + content: , + key: 'available', + }, + { + align: TableCellAlign.Right, + // TODO: provide value via RoundedBalance when it is implemented in the API + content: <>-, + key: 'staked', + }, + { + align: TableCellAlign.Right, + // TODO: provide value via RoundedBalance when it is implemented in the API + content: <>-, + key: 'debonding', + }, + { + align: TableCellAlign.Right, + content: ( + + + + ), + key: 'total', + }, + ], + })) + + return ( + + ) +} diff --git a/src/app/components/AccountSizeBadge/index.tsx b/src/app/components/AccountSizeBadge/index.tsx new file mode 100644 index 000000000..0dc235d15 --- /dev/null +++ b/src/app/components/AccountSizeBadge/index.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' +import Tooltip from '@mui/material/Tooltip' +import { styled } from '@mui/material/styles' +import { FC } from 'react' +import { COLORS } from 'styles/theme/colors' + +const badgeSize = '27px' + +export const StyledBox = styled(Box)(() => ({ + background: 'linear-gradient(88deg, #9747FF 1.71%, #3332CB 79.56%)', + border: `2px solid ${COLORS.brandExtraDark}`, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: badgeSize, + minWidth: badgeSize, + height: badgeSize, + borderRadius: badgeSize, + color: COLORS.white, + fontSize: '10px', + fontWeight: 700, +})) + +type AccountSizeBadgeProps = { + size: string +} + +export const AccountSizeBadge: FC = ({ size }) => { + const { t } = useTranslation() + + return ( + + {size} + + ) +} diff --git a/src/app/pages/ConsensusDashboardPage/AccountsCard.tsx b/src/app/pages/ConsensusDashboardPage/AccountsCard.tsx new file mode 100644 index 000000000..392e2d600 --- /dev/null +++ b/src/app/pages/ConsensusDashboardPage/AccountsCard.tsx @@ -0,0 +1,49 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import { Link as RouterLink } from 'react-router-dom' +import Link from '@mui/material/Link' +import { useGetConsensusAccounts } from '../../../oasis-nexus/api' +import { NUMBER_OF_ITEMS_ON_DASHBOARD } from '../../config' +import { COLORS } from '../../../styles/theme/colors' +import { SearchScope } from '../../../types/searchScope' +import { AccountList } from 'app/components/AccountList' + +const limit = NUMBER_OF_ITEMS_ON_DASHBOARD + +export const AccountsCard: FC<{ scope: SearchScope }> = ({ scope }) => { + const { t } = useTranslation() + const { network } = scope + // TODO: Add query param to sort by rank when API is ready + const accountsQuery = useGetConsensusAccounts(network, { limit }) + + return ( + + + {t('common.viewAll')} + + } + /> + + + + + ) +} diff --git a/src/app/pages/ConsensusDashboardPage/index.tsx b/src/app/pages/ConsensusDashboardPage/index.tsx index 4f668aabb..6e5d594d5 100644 --- a/src/app/pages/ConsensusDashboardPage/index.tsx +++ b/src/app/pages/ConsensusDashboardPage/index.tsx @@ -12,6 +12,7 @@ import { NetworkProposalsCard } from './NetworkProposalsCard' import { ValidatorsCard } from './Validators' import { ConsensusSnapshot } from './ConsensusSnapshot' import { LatestConsensusBlocks } from './LatestConsensusBlocks' +import { AccountsCard } from './AccountsCard' export const ConsensusDashboardPage: FC = () => { const { isMobile } = useScreenSize() @@ -30,6 +31,7 @@ export const ConsensusDashboardPage: FC = () => { + diff --git a/src/app/utils/helpers.ts b/src/app/utils/helpers.ts index 635c89701..118a0b400 100644 --- a/src/app/utils/helpers.ts +++ b/src/app/utils/helpers.ts @@ -95,3 +95,21 @@ export function fromBaseUnits(valueInBaseUnits: string, decimals: number): strin } export const isValidMnemonic = (candidate: string): boolean => validateMnemonic(candidate) + +export const getAccountSize = (value: bigint) => { + if (value >= 100_000_000_000_000_000n) { + return 'XXL' + } else if (value >= 50_000_000_000_000_000n && value <= 99_999_999_000_000_000n) { + return 'XL' + } else if (value >= 25_000_000_000_000_000n && value <= 49_999_999_000_000_000n) { + return 'L' + } else if (value >= 1_000_000_000_000_000n && value <= 24_999_999_000_000_000n) { + return 'M' + } else if (value >= 500_000_000_000_000n && value <= 999_999_000_000_000n) { + return 'S' + } else if (value >= 100_000_000_000_000n && value <= 499_99_000_000_0009n) { + return 'XS' + } else { + return 'XXS' + } +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e3dcd25e6..77050b800 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1,16 +1,22 @@ { "appName": "Oasis Explorer", "account": { + "available": "Available", "cantLoadDetails": "Unfortunately we couldn't load the account details at this time. Please try again later.", + "debonding": "Debonding", "emptyTokenList": "This account holds no {{spec}} {{description}}.", "emptyTransactionList": "There are no transactions on record for this account.", "emptyTokenTransferList": "There are no token transfers on record for this account.", "ERC20": "ERC-20", "ERC721": "ERC-721", + "listTitle": "Accounts", "noTokens": "This account holds no tokens", "showMore": "+ {{counter}} more", + "sizeTooltip": "This account is indicated as an {{size}} account based on sum of assets they own.", + "staked": "Staked", "title": "Account", "transactionsListTitle": "Account Transactions", + "totalBalance": "Total Balance", "totalReceived": "Total Received", "totalSent": "Total Sent" }, diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index 7e25702d9..bd1e14cac 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -14,7 +14,7 @@ import { RuntimeAccount, RuntimeEventType, } from './generated/api' -import { fromBaseUnits, getEthAddressForAccount } from '../app/utils/helpers' +import { fromBaseUnits, getEthAddressForAccount, getAccountSize } from '../app/utils/helpers' import { Network } from '../types/network' import { SearchScope } from '../types/searchScope' import { getTickerForNetwork, NativeTicker } from '../types/ticker' @@ -55,6 +55,8 @@ declare module './generated/api' { network: Network layer: Layer ticker: NativeTicker + size: string + total: string } export interface RuntimeAccount { @@ -902,3 +904,42 @@ export const useGetConsensusValidators: typeof generated.useGetConsensusValidato }, }) } + +export const useGetConsensusAccounts: typeof generated.useGetConsensusAccounts = ( + network, + params?, + options?, +) => { + const ticker = getTickerForNetwork(network) + return generated.useGetConsensusAccounts(network, params, { + ...options, + request: { + ...options?.request, + transformResponse: [ + ...arrayify(axios.defaults.transformResponse), + (data: generated.AccountList, headers, status) => { + if (status !== 200) return data + return { + ...data, + accounts: data.accounts.map(account => { + // TODO: remove when API returns total value, looking at query filters we store that data in Nexus + // or sum proper fields when they are available. Currently API does not return correct fields in accounts list endpoint + // https://github.com/oasisprotocol/explorer/pull/1160#discussion_r1459577303 + const total = BigInt(account.available) + return { + ...account, + available: fromBaseUnits(account.available, consensusDecimals), + total: fromBaseUnits(total.toString(), consensusDecimals), + layer: Layer.consensus, + network, + size: getAccountSize(total), + ticker, + } + }), + } + }, + ...arrayify(options?.request?.transformResponse), + ], + }, + }) +}