Commit daf3fce3 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #228 from blockscout/block-details-api

block details api integration
parents 72ae4ea7 30131a3b
/* eslint-disable max-len */
export const block = {
height: 15006918,
size: 1754,
timestamp: 1662623567695,
transactionsNum: 99,
miner: {
name: 'Alex Emelyanov',
address: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41',
},
minedIn: 20,
reward: {
'static': 2,
tx_fee: 0.1550895290904872,
},
burnt_fees: 0.15116230256264004,
gas_limit: 30000000,
gas_used: 12866397,
gas_target: 14266397,
base_fee_per_gas: 11.748611718,
data: {
hex: '0x7370657468303367c18300',
utf: 'speth03g��',
},
difficulty: '14,735,262,741,740,473',
totalDifficulty: '52,307,701,288,535,570,059,059',
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
parent_hash: '0x4585ac59fdfd84443d93c9216e5172f57ca204639d3e68d1908088b0ebd3e709',
parent_height: 29471346,
sha3_uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
state_root: '0x434fb3f234c8cffc1d915ad26ea360b0261d183ad37af89e31b839df4dd0b00f',
nonce: '0xde80e5b4dd984384',
};
/* eslint-disable max-len */
import { block } from './block';
export const blocks = [
{
...block,
timestamp: Date.now() - 25_000,
},
{
...block,
height: 15006917,
timestamp: Date.now() - 1_000 * 60 * 2,
miner: {
address: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41',
name: undefined,
},
transactionsNum: 185,
size: 452,
gas_limit: 30000000,
gas_used: 15671326,
gas_target: 14671326,
burnt_fees: 0.3988042215537949,
},
{
...block,
height: 15006916,
timestamp: Date.now() - 1_000 * 60 * 60 * 17,
transactionsNum: 377,
size: 5222,
gas_limit: 30000000,
gas_used: 23856751,
gas_target: 28856751,
burnt_fees: 0.0000019660909367,
},
];
import BigNumber from 'bignumber.js';
import type { Block } from 'types/api/block';
export default function getBlockReward(block: Block) {
const txFees = BigNumber(block.tx_fees || 0);
const burntFees = BigNumber(block.burnt_fees || 0);
const minerReward = block.rewards?.find(({ type }) => type === 'Miner Reward' || type === 'Validator Reward')?.reward;
const totalReward = BigNumber(minerReward || 0);
const staticReward = totalReward.minus(txFees).plus(burntFees);
return {
totalReward,
staticReward,
txFees,
burntFees,
};
}
...@@ -3,3 +3,4 @@ import BigNumber from 'bignumber.js'; ...@@ -3,3 +3,4 @@ import BigNumber from 'bignumber.js';
export const WEI = new BigNumber(10 ** 18); export const WEI = new BigNumber(10 ** 18);
export const GWEI = new BigNumber(10 ** 9); export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI); export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
export const ZERO = new BigNumber(0);
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;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/blocks${ req.query.type ? `?type=${ req.query.type }` : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -136,6 +136,10 @@ const variantSubtle = defineStyle((props) => { ...@@ -136,6 +136,10 @@ const variantSubtle = defineStyle((props) => {
color: mode('blackAlpha.800', 'whiteAlpha.800')(props), color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
_hover: { _hover: {
color: 'blue.400', color: 'blue.400',
_disabled: {
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
},
}, },
}; };
} }
......
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward'; import type { Reward } from 'types/api/reward';
export type BlockType = 'block' | 'reorg' | 'uncle';
export interface Block { export interface Block {
height: number; height: number;
timestamp: string; timestamp: string;
...@@ -9,10 +11,10 @@ export interface Block { ...@@ -9,10 +11,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 | null;
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;
...@@ -23,12 +25,12 @@ export interface Block { ...@@ -23,12 +25,12 @@ export interface Block {
gas_target_percentage: number | null; gas_target_percentage: number | null;
gas_used_percentage: number | null; gas_used_percentage: number | null;
burnt_fees_percentage: number | null; burnt_fees_percentage: number | null;
type: 'block' | 'reorg' | 'uncle'; type: BlockType;
tx_fees: string | null; tx_fees: string | null;
uncles_hashes: Array<string>; uncles_hashes: Array<string>;
} }
export interface BlockResponse { export interface BlocksResponse {
items: Array<Block>; items: Array<Block>;
next_page_params: { next_page_params: {
block_number: number; block_number: number;
......
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, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
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 { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import link from 'lib/link/link'; import link from 'lib/link/link';
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';
...@@ -22,6 +31,15 @@ import Utilization from 'ui/shared/Utilization'; ...@@ -22,6 +31,15 @@ import Utilization from 'ui/shared/Utilization';
const BlockDetails = () => { const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, 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);
...@@ -31,7 +49,25 @@ const BlockDetails = () => { ...@@ -31,7 +49,25 @@ const BlockDetails = () => {
}); });
}, []); }, []);
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
const increment = direction === 'next' ? +1 : -1;
const nextId = String(Number(router.query.id) + increment);
const url = link('block', { id: nextId });
router.push(url, undefined);
}, [ router ]);
if (isLoading) {
return <BlockDetailsSkeleton/>;
}
if (isError) {
const is404 = error?.error?.status === 404;
return is404 ? <Alert>This block has not been processed yet.</Alert> : <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">
...@@ -39,30 +75,37 @@ const BlockDetails = () => { ...@@ -39,30 +75,37 @@ 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 }/> { data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> }
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous block"
nextLabel="View next block"
isPrevDisabled={ data.height === 0 }
/>
</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', { id: router.query.id }, { tab: 'transactions' }) }> <Link href={ link('block', { id: router.query.id }, { tab: 'transactions' }) }>
{ block.transactionsNum } transactions { data.tx_count } transactions
</Link> </Link>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
...@@ -70,10 +113,12 @@ const BlockDetails = () => { ...@@ -70,10 +113,12 @@ 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>
{ !totalReward.isEqualTo(ZERO) && (
<DetailsInfoItem <DetailsInfoItem
title="Block reward" title="Block reward"
hint={ hint={
...@@ -82,21 +127,32 @@ const BlockDetails = () => { ...@@ -82,21 +127,32 @@ const BlockDetails = () => {
} }
columnGap={ 1 } columnGap={ 1 }
> >
<Text>{ block.reward.static + block.reward.tx_fee - block.burnt_fees }</Text> <Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
{ (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && (
<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>{ staticReward.dividedBy(WEI).toFixed() }</span>
</Tooltip> </Tooltip>
{ !txFees.isEqualTo(ZERO) && (
<>
{ space }+{ space } { space }+{ space }
<Tooltip label="Txn fees"> <Tooltip label="Txn fees">
<span>{ block.reward.tx_fee }</span> <span>{ txFees.dividedBy(WEI).toFixed() }</span>
</Tooltip> </Tooltip>
</>
) }
{ !burntFees.isEqualTo(ZERO) && (
<>
{ space }-{ space } { space }-{ space }
<Tooltip label="Burnt fees"> <Tooltip label="Burnt fees">
<span>{ block.burnt_fees }</span> <span>{ burntFees.dividedBy(WEI).toFixed() }</span>
</Tooltip> </Tooltip>
</>
) }
)</Text> )</Text>
) }
</DetailsInfoItem> </DetailsInfoItem>
) }
{ sectionGap } { sectionGap }
...@@ -104,43 +160,69 @@ const BlockDetails = () => { ...@@ -104,43 +160,69 @@ 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>{ BigNumber(data.gas_used || 0).toFormat() }</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={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
/>
<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>{ BigNumber(data.gas_limit).toFormat() }</Text>
</DetailsInfoItem> </DetailsInfoItem>
{ data.base_fee_per_gas && (
<DetailsInfoItem <DetailsInfoItem
title="Base fee per gas" title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion." 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 }) } { appConfig.network.currency } </Text> <Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency } </Text>
<Text variant="secondary" whiteSpace="pre">{ space }({ block.base_fee_per_gas.toLocaleString('en', { minimumFractionDigits: 9 }) } Gwei)</Text> <Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
</Text>
</DetailsInfoItem> </DetailsInfoItem>
) }
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency || 'native token' } burned from transactions included in the block. hint={
Equals Block Base Fee per Gas * Gas Used.` } `Amount of ${ appConfig.network.currency || 'native token' } burned from transactions included in the block.
Equals Block Base Fee per Gas * Gas Used.`
}
> >
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ block.burnt_fees.toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency }</Text> <Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
{ !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label="Burnt fees / Txn fees * 100%">
<Box> <Box>
<Utilization ml={ 4 } value={ block.burnt_fees / block.reward.tx_fee }/> <Utilization
ml={ 4 }
value={ burntFees.dividedBy(txFees).toNumber() }
/>
</Box> </Box>
</Tooltip> </Tooltip>
) }
</DetailsInfoItem> </DetailsInfoItem>
{ data.priority_fee && (
<DetailsInfoItem <DetailsInfoItem
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</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 }}>
...@@ -167,13 +249,13 @@ const BlockDetails = () => { ...@@ -167,13 +249,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 } { BigNumber(data.difficulty).toFormat() }
</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 } { BigNumber(data.total_difficulty).toFormat() }
</DetailsInfoItem> </DetailsInfoItem>
{ sectionGap } { sectionGap }
...@@ -184,30 +266,44 @@ const BlockDetails = () => { ...@@ -184,30 +266,44 @@ 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>
{ data.height > 0 && (
<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>
{ data.rewards
?.filter(({ type }) => type !== 'Validator Reward' && type !== 'Miner Reward')
.map(({ type, reward }) => (
<DetailsInfoItem
key={ type }
title={ type }
hint="Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees."
>
{ BigNumber(reward).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</DetailsInfoItem> </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;
import { Box, Text, Show } from '@chakra-ui/react'; import { Box, Text, Show, Alert, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { BlockType, BlocksResponse } from 'types/api/block';
import useFetch from 'lib/hooks/useFetch';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable'; import BlocksTable from 'ui/blocks/BlocksTable';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
interface Props {
type?: BlockType;
}
const BlocksContent = ({ type }: Props) => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ 'blocks', type ],
async() => await fetch(`/api/blocks${ type ? `?type=${ type }` : '' }`),
);
if (isLoading) {
return (
<>
<Show below="lg" key="skeleton-mobile">
<BlocksSkeletonMobile/>
</Show>
<Show above="lg" key="skeleton-desktop">
<Skeleton h={ 6 } mb={ 8 } w="150px"/>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Alert>There are no blocks.</Alert>;
}
const BlocksContent = () => {
return ( return (
<> <>
<Text>Total of 15,044,883 blocks</Text> <Text>Total of { data.items[0].height.toLocaleString() } blocks</Text>
<Show below="lg"><BlocksList/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
<Show above="lg"><BlocksTable/></Show> <Show above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}> <Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/> <Pagination currentPage={ 1 }/>
</Box> </Box>
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { blocks } from 'data/blocks'; import type { Block } from 'types/api/block';
import BlocksListItem from 'ui/blocks/BlocksListItem'; import BlocksListItem from 'ui/blocks/BlocksListItem';
const BlocksList = () => { interface Props {
data: Array<Block>;
}
const BlocksList = ({ data }: Props) => {
return ( return (
<Box mt={ 8 }> <Box mt={ 8 }>
{ blocks.map((item, index) => <BlocksListItem key={ item.height } data={ item } isPending={ index === 0 }/>) } { data.map((item) => <BlocksListItem key={ item.height } data={ item }/>) }
</Box> </Box>
); );
}; };
......
import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react'; import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type { Block } from 'types/api/block';
import type { blocks } from 'data/blocks';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
...@@ -14,12 +16,13 @@ import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; ...@@ -14,12 +16,13 @@ import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
interface Props { interface Props {
data: ArrayElement<typeof blocks>; data: Block;
isPending?: boolean; isPending?: boolean;
} }
const BlocksListItem = ({ data, isPending }: Props) => { const BlocksListItem = ({ data, isPending }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const { totalReward, burntFees, txFees } = getBlockReward(data);
return ( return (
<AccountListItemMobile rowGap={ 3 }> <AccountListItemMobile rowGap={ 3 }>
...@@ -41,29 +44,29 @@ const BlocksListItem = ({ data, isPending }: Props) => { ...@@ -41,29 +44,29 @@ const BlocksListItem = ({ data, isPending }: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Miner</Text> <Text fontWeight={ 500 }>Miner</Text>
<AddressLink alias={ data.miner?.name } hash={ data.miner.address } truncation="constant"/> <AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text> <Text fontWeight={ 500 }>Txn</Text>
<Text variant="secondary">{ data.transactionsNum }</Text> <Text variant="secondary">{ data.tx_count }</Text>
</Flex> </Flex>
<Box> <Box>
<Text fontWeight={ 500 }>Gas used</Text> <Text fontWeight={ 500 }>Gas used</Text>
<Flex columnGap={ 4 }> <Flex columnGap={ 4 }>
<Text variant="secondary">{ data.gas_used.toLocaleString('en') }</Text> <Text variant="secondary">{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ data.gas_used / data.gas_limit }/> <Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
<GasUsedToTargetRatio used={ data.gas_used } target={ data.gas_target }/> <GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
</Flex> </Flex>
</Box> </Box>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text>
<Text variant="secondary">{ (data.reward.static + data.reward.tx_fee - data.burnt_fees).toLocaleString('en', { maximumFractionDigits: 5 }) }</Text> <Text variant="secondary">{ totalReward.div(WEI).toFixed() }</Text>
</Flex> </Flex>
<Flex> <Flex>
<Text fontWeight={ 500 }>Burnt fees</Text> <Text fontWeight={ 500 }>Burnt fees</Text>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" ml={ 2 }/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500" ml={ 2 }/>
<Text variant="secondary" ml={ 1 }>{ data.burnt_fees.toLocaleString('en', { maximumFractionDigits: 6 }) }</Text> <Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
<Utilization ml={ 4 } value={ data.burnt_fees / data.reward.tx_fee }/> <Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex> </Flex>
</AccountListItemMobile> </AccountListItemMobile>
); );
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const BlocksSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 } justifyContent="space-between">
<Skeleton w="75px"/>
<Skeleton w="90px"/>
</Flex>
<Skeleton h={ 6 } w="130px"/>
<Skeleton h={ 6 } w="180px"/>
<Skeleton h={ 6 } w="60px"/>
<Skeleton h={ 6 } w="100%"/>
<Skeleton h={ 6 } w="170px"/>
<Skeleton h={ 6 } w="100%"/>
</Flex>
)) }
</Box>
);
};
export default BlocksSkeletonMobile;
...@@ -2,27 +2,32 @@ import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react'; ...@@ -2,27 +2,32 @@ import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import { blocks } from 'data/blocks'; import type { Block } from 'types/api/block';
import BlocksTableItem from 'ui/blocks/BlocksTableItem'; import BlocksTableItem from 'ui/blocks/BlocksTableItem';
const BlocksTable = () => { interface Props {
data: Array<Block>;
}
const BlocksTable = ({ data }: Props) => {
return ( return (
<TableContainer width="100%" mt={ 8 }> <TableContainer width="100%" mt={ 8 }>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead> <Thead>
<Tr> <Tr>
<Th width="124px">Block</Th> <Th width="125px">Block</Th>
<Th width="112px">Size</Th> <Th width="120px">Size</Th>
<Th width="144px">Miner</Th> <Th width="21%" minW="144px">Miner</Th>
<Th width="64px" isNumeric>Txn</Th> <Th width="64px" isNumeric>Txn</Th>
<Th width="40%">Gas used</Th> <Th width="35%">Gas used</Th>
<Th width="30%">Reward { appConfig.network.currency }</Th> <Th width="22%">Reward { appConfig.network.currency }</Th>
<Th width="30%">Burnt fees { appConfig.network.currency }</Th> <Th width="22%">Burnt fees { appConfig.network.currency }</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ blocks.map((item, index) => <BlocksTableItem key={ item.height } data={ item } isPending={ index === 0 }/>) } { data.map((item) => <BlocksTableItem key={ item.height } data={ item }/>) }
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>
......
import { Tr, Td, Text, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Tr, Td, Text, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type { Block } from 'types/api/block';
import type { blocks } from 'data/blocks';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -12,46 +14,50 @@ import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; ...@@ -12,46 +14,50 @@ import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
interface Props { interface Props {
data: ArrayElement<typeof blocks>; data: Block;
isPending?: boolean; isPending?: boolean;
} }
const BlocksTableItem = ({ data, isPending }: Props) => { const BlocksTableItem = ({ data, isPending }: Props) => {
const { totalReward, burntFees, txFees } = getBlockReward(data);
return ( return (
<Tr> <Tr>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> } { isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<Link <Link
fontWeight={ 600 } fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) } href={ link('block', { id: String(data.height) }) }
> >
{ data.height } { data.height }
</Link> </Link>
</Tooltip>
</Flex> </Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text> <Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td> <Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<AddressLink alias={ data.miner?.name } hash={ data.miner.address } truncation="constant"/> <AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td> </Td>
<Td isNumeric fontSize="sm">{ data.transactionsNum }</Td> <Td isNumeric fontSize="sm">{ data.tx_count }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<Box>{ data.gas_used.toLocaleString('en') }</Box> <Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Flex mt={ 2 }> <Flex mt={ 2 }>
<Utilization colorScheme="gray" value={ data.gas_used / data.gas_limit }/> <Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
<GasUsedToTargetRatio ml={ 2 } used={ data.gas_used } target={ data.gas_target }/> <GasUsedToTargetRatio ml={ 2 } value={ data.gas_target_percentage || undefined }/>
</Flex> </Flex>
</Td> </Td>
<Td fontSize="sm">{ (data.reward.static + data.reward.tx_fee - data.burnt_fees).toLocaleString('en', { maximumFractionDigits: 5 }) }</Td> <Td fontSize="sm">{ totalReward.dividedBy(WEI).toFixed() }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }> <Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/> <Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/>
{ data.burnt_fees.toLocaleString('en', { maximumFractionDigits: 6 }) } { burntFees.dividedBy(WEI).toFixed(8) }
</Flex> </Flex>
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label="Burnt fees / Txn fees * 100%">
<Box> <Box w="min-content">
<Utilization mt={ 2 } value={ data.burnt_fees / data.reward.tx_fee }/> <Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
</Box> </Box>
</Tooltip> </Tooltip>
</Td> </Td>
......
...@@ -17,9 +17,13 @@ const TABS: Array<RoutedTab> = [ ...@@ -17,9 +17,13 @@ const TABS: Array<RoutedTab> = [
const BlockPageContent = () => { const BlockPageContent = () => {
const router = useRouter(); const router = useRouter();
if (!router.query.id) {
return null;
}
return ( return (
<Page> <Page>
<PageTitle text={ `Block #${ router.query.id || '' }` }/> <PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs <RoutedTabs
tabs={ TABS } tabs={ TABS }
/> />
......
...@@ -9,8 +9,8 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; ...@@ -9,8 +9,8 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'blocks', title: 'All', component: <BlocksContent/> }, { id: 'blocks', title: 'All', component: <BlocksContent/> },
{ id: 'reorgs', title: 'Forked', component: <BlocksContent/> }, { id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg"/> },
{ id: 'uncles', title: 'Uncles', component: <BlocksContent/> }, { id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle"/> },
]; ];
const BlocksPageContent = () => { const BlocksPageContent = () => {
......
import { Stat, StatArrow, Text, chakra } from '@chakra-ui/react'; import { Stat, StatArrow, Text, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { type Props = ({
value: number;
} | {
used: number; used: number;
target: number; target: number;
}) & {
className?: string; className?: string;
} }
const GasUsedToTargetRatio = ({ used, target, className }: Props) => { const GasUsedToTargetRatio = (props: Props) => {
const percentage = (used / target - 1) * 100; const percentage = (() => {
if ('value' in props) {
return props.value;
}
return (props.used / props.target - 1) * 100;
})();
return ( return (
<Stat className={ className }> <Stat className={ props.className }>
<StatArrow type={ percentage >= 0 ? 'increase' : 'decrease' }/> <StatArrow type={ percentage >= 0 ? 'increase' : 'decrease' }/>
<Text as="span" color={ percentage >= 0 ? 'green.500' : 'red.500' } fontWeight={ 600 }> <Text as="span" color={ percentage >= 0 ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ Math.abs(percentage).toLocaleString('en', { maximumFractionDigits: 2 }) } % { Math.abs(percentage).toLocaleString('en', { maximumFractionDigits: 2 }) } %
......
import { Box, Icon, IconButton, chakra } from '@chakra-ui/react'; import { Box, Icon, IconButton, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrow from 'icons/arrows/east-mini.svg'; import eastArrow from 'icons/arrows/east-mini.svg';
interface Props { interface Props {
className?: string; className?: string;
onClick: (direction: 'prev' | 'next') => void;
prevLabel?: string;
nextLabel?: string;
isPrevDisabled?: boolean;
isNextDisabled?: boolean;
} }
const PrevNext = ({ className }: Props) => { const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled }: Props) => {
const handelPrevClick = React.useCallback(() => {
onClick('prev');
}, [ onClick ]);
const handelNextClick = React.useCallback(() => {
onClick('next');
}, [ onClick ]);
return ( return (
<Box className={ className }> <Box className={ className }>
<Tooltip label={ prevLabel }>
<IconButton <IconButton
aria-label="prev" aria-label="prev"
icon={ <Icon as={ eastArrow } boxSize={ 6 }/> } icon={ <Icon as={ eastArrow } boxSize={ 6 }/> }
...@@ -17,7 +31,11 @@ const PrevNext = ({ className }: Props) => { ...@@ -17,7 +31,11 @@ const PrevNext = ({ className }: Props) => {
borderRadius="sm" borderRadius="sm"
variant="subtle" variant="subtle"
colorScheme="gray" colorScheme="gray"
onClick={ handelPrevClick }
disabled={ isPrevDisabled }
/> />
</Tooltip>
<Tooltip label={ nextLabel }>
<IconButton <IconButton
aria-label="next" aria-label="next"
icon={ <Icon as={ eastArrow }boxSize={ 6 } transform="rotate(180deg)"/> } icon={ <Icon as={ eastArrow }boxSize={ 6 } transform="rotate(180deg)"/> }
...@@ -26,7 +44,10 @@ const PrevNext = ({ className }: Props) => { ...@@ -26,7 +44,10 @@ const PrevNext = ({ className }: Props) => {
variant="subtle" variant="subtle"
colorScheme="gray" colorScheme="gray"
ml="10px" ml="10px"
onClick={ handelNextClick }
disabled={ isNextDisabled }
/> />
</Tooltip>
</Box> </Box>
); );
}; };
......
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react';
import clamp from 'lodash/clamp';
import React from 'react'; import React from 'react';
interface Props { interface Props {
...@@ -10,7 +11,7 @@ interface Props { ...@@ -10,7 +11,7 @@ interface Props {
const WIDTH = 50; const WIDTH = 50;
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => { const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
const valueString = (value * 100).toLocaleString('en', { maximumFractionDigits: 2 }) + '%'; const valueString = (clamp(value * 100, 0, 100)).toLocaleString('en', { maximumFractionDigits: 2 }) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.500'); const colorGrayScheme = useColorModeValue('gray.500', 'gray.500');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500'; const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
......
...@@ -24,7 +24,7 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -24,7 +24,7 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import PrevNext from 'ui/shared/PrevNext'; // import PrevNext from 'ui/shared/PrevNext';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
// import TokenSnippet from 'ui/shared/TokenSnippet'; // import TokenSnippet from 'ui/shared/TokenSnippet';
...@@ -84,7 +84,8 @@ const TxDetails = () => { ...@@ -84,7 +84,8 @@ const TxDetails = () => {
<HashStringShortenDynamic hash={ data.hash }/> <HashStringShortenDynamic hash={ data.hash }/>
</Box> </Box>
<CopyToClipboard text={ data.hash }/> <CopyToClipboard text={ data.hash }/>
<PrevNext ml={ 7 }/> { /* api doesn't support navigation between certain address account tx */ }
{ /* <PrevNext ml={ 7 }/> */ }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Status" title="Status"
......
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