Commit ac627348 authored by tom's avatar tom

block data

parent 95d1bcbb
import { utils } from 'ethers';
import type { Block } from 'types/api/block';
export default function getBlockReward(block: Block) {
const txFees = utils.parseUnits(block.tx_fees || '0', 'wei');
const burntFees = utils.parseUnits(String(block.burnt_fees || '0'), 'wei');
const totalReward = utils.parseUnits(String(block.rewards?.find(({ type }) => type === 'Miner Reward' || type === 'Validator Reward')?.reward) || '0', 'wei');
const staticReward = totalReward.sub(txFees).add(burntFees);
return {
totalReward,
staticReward,
txFees,
burntFees,
};
}
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/blocks/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -9,10 +9,10 @@ export interface Block { ...@@ -9,10 +9,10 @@ export interface Block {
size: number; size: number;
hash: string; hash: string;
parent_hash: string; parent_hash: string;
difficulty: number; difficulty: string;
total_difficulty: number; total_difficulty: string;
gas_used: number; gas_used: string;
gas_limit: number; gas_limit: string;
nonce: number; nonce: number;
base_fee_per_gas: number | null; base_fee_per_gas: number | null;
burnt_fees: number | null; burnt_fees: number | null;
......
export interface Reward { export interface Reward {
reward: number; reward: number;
type: 'Miner Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward'; type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
} }
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react'; import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { utils } from 'ethers';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import { block } from 'data/block'; import type { Block } from 'types/api/block';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
...@@ -24,6 +31,15 @@ const BlockDetails = () => { ...@@ -24,6 +31,15 @@ const BlockDetails = () => {
const link = useLink(); const link = useLink();
const router = useRouter(); const router = useRouter();
const network = useNetwork(); const network = useNetwork();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Block>(
[ 'block', router.query.id ],
async() => await fetch(`/api/blocks/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag); setIsExpanded((flag) => !flag);
...@@ -33,7 +49,16 @@ const BlockDetails = () => { ...@@ -33,7 +49,16 @@ const BlockDetails = () => {
}); });
}, []); }, []);
if (isLoading) {
return <BlockDetailsSkeleton/>;
}
if (isError) {
return <DataFetchAlert/>;
}
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>; const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
...@@ -41,30 +66,30 @@ const BlockDetails = () => { ...@@ -41,30 +66,30 @@ const BlockDetails = () => {
title="Block height" title="Block height"
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain." hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain."
> >
{ block.height } { data.height }
<PrevNext ml={ 6 }/> <PrevNext ml={ 6 }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Size" title="Size"
hint="Size of the block in bytes." hint="Size of the block in bytes."
> >
{ block.size.toLocaleString('en') } { data.size.toLocaleString('en') }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Timestamp" title="Timestamp"
hint="Date & time at which block was produced." hint="Date & time at which block was produced."
> >
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ dayjs(block.timestamp).fromNow() }</Text> <Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text>
<TextSeparator/> <TextSeparator/>
<Text whiteSpace="normal">{ dayjs(block.timestamp).format('LLLL') }</Text> <Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="The number of transactions in the block." hint="The number of transactions in the block."
> >
<Link href={ link('block_txs', { id: router.query.id }) }> <Link href={ link('block_txs', { id: router.query.id }) }>
{ block.transactionsNum } transactions { data.tx_count } transactions
</Link> </Link>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
...@@ -72,9 +97,10 @@ const BlockDetails = () => { ...@@ -72,9 +97,10 @@ const BlockDetails = () => {
hint="A block producer who successfully included the block onto the blockchain." hint="A block producer who successfully included the block onto the blockchain."
columnGap={ 1 } columnGap={ 1 }
> >
<AddressLink hash={ block.miner.address }/> <AddressLink hash={ data.miner.hash }/>
{ block.miner.name && <Text>(Miner: { block.miner.name })</Text> } { data.miner.name && <Text>(Miner: { data.miner.name })</Text> }
<Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Block reward" title="Block reward"
...@@ -84,18 +110,18 @@ const BlockDetails = () => { ...@@ -84,18 +110,18 @@ const BlockDetails = () => {
} }
columnGap={ 1 } columnGap={ 1 }
> >
<Text>{ block.reward.static + block.reward.tx_fee - block.burnt_fees }</Text> <Text>{ utils.formatUnits(totalReward) } { network?.currency }</Text>
<Text variant="secondary" whiteSpace="break-spaces">( <Text variant="secondary" whiteSpace="break-spaces">(
<Tooltip label="Static block reward"> <Tooltip label="Static block reward">
<span>{ block.reward.static }</span> <span>{ utils.formatUnits(staticReward) }</span>
</Tooltip> </Tooltip>
{ space }+{ space } { space }+{ space }
<Tooltip label="Txn fees"> <Tooltip label="Txn fees">
<span>{ block.reward.tx_fee }</span> <span>{ utils.formatUnits(txFees) }</span>
</Tooltip> </Tooltip>
{ space }-{ space } { space }-{ space }
<Tooltip label="Burnt fees"> <Tooltip label="Burnt fees">
<span>{ block.burnt_fees }</span> <span>{ utils.formatUnits(burntFees) }</span>
</Tooltip> </Tooltip>
)</Text> )</Text>
</DetailsInfoItem> </DetailsInfoItem>
...@@ -106,42 +132,62 @@ const BlockDetails = () => { ...@@ -106,42 +132,62 @@ const BlockDetails = () => {
title="Gas used" title="Gas used"
hint="The total gas amount used in the block and its percentage of gas filled in the block." hint="The total gas amount used in the block and its percentage of gas filled in the block."
> >
<Text>{ block.gas_used.toLocaleString('en') }</Text> <Text>{ utils.commify(data.gas_used) }</Text>
<Utilization ml={ 4 } mr={ 5 } colorScheme="gray" value={ block.gas_used / block.gas_limit }/> <Utilization
<GasUsedToTargetRatio used={ block.gas_used } target={ block.gas_target }/> ml={ 4 }
mr={ 5 }
colorScheme="gray"
value={ utils.parseUnits(data.gas_used).mul(10_000).div(utils.parseUnits(data.gas_limit)).toNumber() / 10_000 }
/>
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas limit" title="Gas limit"
hint="Total gas limit provided by all transactions in the block." hint="Total gas limit provided by all transactions in the block."
> >
<Text>{ block.gas_limit.toLocaleString('en') }</Text> <Text>{ utils.commify(data.gas_limit) }</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem { data.base_fee_per_gas && (
title="Base fee per gas" <DetailsInfoItem
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion." title="Base fee per gas"
> hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
<Text>{ (block.base_fee_per_gas / 10 ** 9).toLocaleString('en', { minimumFractionDigits: 18 }) } { network?.currency } </Text> >
<Text variant="secondary" whiteSpace="pre">{ space }({ block.base_fee_per_gas.toLocaleString('en', { minimumFractionDigits: 9 }) } Gwei)</Text> <Text>{ utils.formatUnits(utils.parseUnits(String(data.base_fee_per_gas), 'wei')) } { network?.currency } </Text>
</DetailsInfoItem> <Text variant="secondary" whiteSpace="pre">
<DetailsInfoItem { space }({ utils.formatUnits(utils.parseUnits(String(data.base_fee_per_gas), 'wei'), 'gwei') } Gwei)
title="Burnt fees" </Text>
hint={ `Amount of ${ network?.currency || 'native token' } burned from transactions included in the block. Equals Block Base Fee per Gas * Gas Used.` } </DetailsInfoItem>
> ) }
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> { data.burnt_fees && (
<Text ml={ 1 }>{ block.burnt_fees.toLocaleString('en', { minimumFractionDigits: 18 }) } { network?.currency }</Text> <DetailsInfoItem
<Tooltip label="Burnt fees / Txn fees * 100%"> title="Burnt fees"
<Box> hint={
<Utilization ml={ 4 } value={ block.burnt_fees / block.reward.tx_fee }/> `Amount of ${ network?.currency || 'native token' } burned from transactions included in the block.
</Box> Equals Block Base Fee per Gas * Gas Used.`
</Tooltip> }
</DetailsInfoItem> >
<DetailsInfoItem <Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ utils.formatUnits(burntFees) } { network?.currency }</Text>
{ data.tx_fees && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
<Utilization
ml={ 4 }
value={ burntFees.mul(10_000).div(txFees).toNumber() / 10_000 }
/>
</Box>
</Tooltip>
) }
</DetailsInfoItem>
) }
{ /* api doesn't support extra data yet */ }
{ /* <DetailsInfoItem
title="Extra data" title="Extra data"
hint="Any data that can be included by the miner in the block." hint="Any data that can be included by the miner in the block."
> >
<Text whiteSpace="pre">{ block.data.utf } </Text> <Text whiteSpace="pre">{ data.extra_data } </Text>
<Text variant="secondary">(Hex: { block.data.hex })</Text> <Text variant="secondary">(Hex: { data.extra_data })</Text>
</DetailsInfoItem> </DetailsInfoItem> */ }
{ /* CUT */ } { /* CUT */ }
<GridItem colSpan={{ base: undefined, lg: 2 }}> <GridItem colSpan={{ base: undefined, lg: 2 }}>
...@@ -168,13 +214,13 @@ const BlockDetails = () => { ...@@ -168,13 +214,13 @@ const BlockDetails = () => {
title="Difficulty" title="Difficulty"
hint="Block difficulty for miner, used to calibrate block generation time." hint="Block difficulty for miner, used to calibrate block generation time."
> >
{ block.difficulty } { utils.commify(data.difficulty) }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Total difficulty" title="Total difficulty"
hint="Total difficulty of the chain until this block." hint="Total difficulty of the chain until this block."
> >
{ block.totalDifficulty } { utils.commify(data.total_difficulty) }
</DetailsInfoItem> </DetailsInfoItem>
{ sectionGap } { sectionGap }
...@@ -185,30 +231,40 @@ const BlockDetails = () => { ...@@ -185,30 +231,40 @@ const BlockDetails = () => {
flexWrap="nowrap" flexWrap="nowrap"
> >
<Box overflow="hidden"> <Box overflow="hidden">
<HashStringShortenDynamic hash={ block.hash }/> <HashStringShortenDynamic hash={ data.hash }/>
</Box> </Box>
<CopyToClipboard text={ block.hash }/> <CopyToClipboard text={ data.hash }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Parent hash" title="Parent hash"
hint="The hash of the block from which this block was generated." hint="The hash of the block from which this block was generated."
flexWrap="nowrap" flexWrap="nowrap"
> >
<AddressLink hash={ block.parent_hash } type="block" id={ String(block.parent_height) }/> <AddressLink hash={ data.parent_hash } type="block" id={ String(data.height - 1) }/>
<CopyToClipboard text={ block.hash }/> <CopyToClipboard text={ data.parent_hash }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem { /* api doesn't support state root yet */ }
{ /* <DetailsInfoItem
title="State root" title="State root"
hint="The root of the state trie." hint="The root of the state trie."
> >
<Text wordBreak="break-all" whiteSpace="break-spaces">{ block.state_root }</Text> <Text wordBreak="break-all" whiteSpace="break-spaces">{ data.state_root }</Text>
</DetailsInfoItem> </DetailsInfoItem> */ }
<DetailsInfoItem <DetailsInfoItem
title="Nonce" title="Nonce"
hint="Block nonce is a value used during mining to demonstrate proof of work for a block." hint="Block nonce is a value used during mining to demonstrate proof of work for a block."
> >
{ block.nonce } { data.nonce }
</DetailsInfoItem> </DetailsInfoItem>
{ data.rewards?.map(({ type, reward }) => (
<DetailsInfoItem
key={ type }
title={ type }
hint="Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees."
>
{ utils.formatUnits(utils.parseUnits(String(reward), 'wei')) } { network?.currency }
</DetailsInfoItem>
)) }
</> </>
) } ) }
</Grid> </Grid>
......
import { Grid, GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const SkeletonRow = ({ w = '100%' }: { w?: string }) => (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
</GridItem>
</>
);
const BlockDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<SkeletonRow w="25%"/>
<SkeletonRow w="25%"/>
<SkeletonRow w="65%"/>
<SkeletonRow w="25%"/>
<SkeletonRow/>
<SkeletonRow/>
{ sectionGap }
<SkeletonRow w="50%"/>
<SkeletonRow w="25%"/>
<SkeletonRow w="50%"/>
<SkeletonRow w="50%"/>
<SkeletonRow w="50%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default BlockDetailsSkeleton;
...@@ -2,13 +2,14 @@ import { Stat, StatArrow, Text, chakra } from '@chakra-ui/react'; ...@@ -2,13 +2,14 @@ import { Stat, StatArrow, Text, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
used: number; used?: number;
target: number; target?: number;
value?: number;
className?: string; className?: string;
} }
const GasUsedToTargetRatio = ({ used, target, className }: Props) => { const GasUsedToTargetRatio = ({ used, target, value, className }: Props) => {
const percentage = (used / target - 1) * 100; const percentage = value || ((used || 0) / (target || 0) - 1) * 100;
return ( return (
<Stat className={ className }> <Stat className={ className }>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment