Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consensus dashboard account list card #1160

Merged
merged 6 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/1160.trivial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Consensus dashboard account list card
82 changes: 82 additions & 0 deletions src/app/components/AccountList/index.tsx
Original file line number Diff line number Diff line change
@@ -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<AccountListProps> = ({ 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: (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<AccountSizeBadge size={account.size} />
</Box>
),
key: 'size',
},
{
content: <AccountLink scope={account} address={account.address} />,
key: 'address',
},
{
align: TableCellAlign.Right,
content: <RoundedBalance value={account.available} ticker={account.ticker} />,
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: (
<strong>
<RoundedBalance value={account.total} ticker={account.ticker} />
</strong>
),
key: 'total',
},
],
}))

return (
<Table
columns={tableColumns}
rows={tableRows}
rowsNumber={limit}
name={t('account.listTitle')}
isLoading={isLoading}
pagination={pagination}
/>
)
}
37 changes: 37 additions & 0 deletions src/app/components/AccountSizeBadge/index.tsx
Original file line number Diff line number Diff line change
@@ -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<AccountSizeBadgeProps> = ({ size }) => {
const { t } = useTranslation()

return (
<Tooltip arrow placement="top" title={t('account.sizeTooltip', { size })}>
<StyledBox>{size}</StyledBox>
</Tooltip>
)
}
49 changes: 49 additions & 0 deletions src/app/pages/ConsensusDashboardPage/AccountsCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<CardHeader
disableTypography
component="h3"
title={t('account.listTitle')}
action={
<Link
component={RouterLink}
// TODO: Update when Accounts page is ready
to={''}
sx={{ color: COLORS.brandDark }}
>
{t('common.viewAll')}
</Link>
}
/>
<CardContent>
<AccountList
accounts={accountsQuery.data?.data.accounts}
isLoading={accountsQuery.isLoading}
limit={limit}
pagination={false}
/>
</CardContent>
</Card>
)
}
2 changes: 2 additions & 0 deletions src/app/pages/ConsensusDashboardPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -30,6 +31,7 @@ export const ConsensusDashboardPage: FC = () => {
</Grid>
</Grid>
<ValidatorsCard scope={scope} />
<AccountsCard scope={scope} />
<NetworkProposalsCard scope={scope} />
<TransactionsStats scope={scope} />
<LearningMaterials />
Expand Down
18 changes: 18 additions & 0 deletions src/app/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
6 changes: 6 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
43 changes: 42 additions & 1 deletion src/oasis-nexus/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -55,6 +55,8 @@ declare module './generated/api' {
network: Network
layer: Layer
ticker: NativeTicker
size: string
total: string
}

export interface RuntimeAccount {
Expand Down Expand Up @@ -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),
],
},
})
}
Loading