mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
skeleton for tx details
This commit is contained in:
+7
-2
@@ -1,10 +1,13 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Head from 'next/head';
|
||||
import type { RoutedQuery } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import getSeo from 'lib/next/tx/getSeo';
|
||||
import Transaction from 'ui/pages/Transaction';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false });
|
||||
|
||||
const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => {
|
||||
const { title, description } = getSeo({ hash });
|
||||
@@ -15,7 +18,9 @@ const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQu
|
||||
<title>{ title }</title>
|
||||
<meta name="description" content={ description }/>
|
||||
</Head>
|
||||
<Transaction/>
|
||||
<Page>
|
||||
<Transaction/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,9 +8,9 @@ import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { useAppContext } from 'lib/appContext';
|
||||
import networkExplorers from 'lib/networks/networkExplorers';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { TX } from 'stubs/tx';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
import LinkExternal from 'ui/shared/LinkExternal';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import TxDetails from 'ui/tx/TxDetails';
|
||||
@@ -38,7 +38,10 @@ const TransactionPageContent = () => {
|
||||
|
||||
const { data } = useApiQuery('tx', {
|
||||
pathParams: { hash },
|
||||
queryOptions: { enabled: Boolean(hash) },
|
||||
queryOptions: {
|
||||
enabled: Boolean(hash),
|
||||
placeholderData: TX,
|
||||
},
|
||||
});
|
||||
|
||||
const explorersLinks = networkExplorers
|
||||
@@ -66,7 +69,7 @@ const TransactionPageContent = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<>
|
||||
<TextAd mb={ 6 }/>
|
||||
<PageTitle
|
||||
text="Transaction details"
|
||||
@@ -75,7 +78,7 @@ const TransactionPageContent = () => {
|
||||
backLinkLabel="Back to transactions list"
|
||||
/>
|
||||
<RoutedTabs tabs={ TABS }/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+74
-43
@@ -11,6 +11,7 @@ import {
|
||||
Tooltip,
|
||||
chakra,
|
||||
useColorModeValue,
|
||||
Skeleton,
|
||||
} from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { route } from 'nextjs-routes';
|
||||
@@ -42,14 +43,13 @@ import TextSeparator from 'ui/shared/TextSeparator';
|
||||
import TxStatus from 'ui/shared/TxStatus';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
import TxDetailsActions from 'ui/tx/details/TxDetailsActions';
|
||||
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
|
||||
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
|
||||
import TxRevertReason from 'ui/tx/details/TxRevertReason';
|
||||
import TxSocketAlert from 'ui/tx/TxSocketAlert';
|
||||
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
|
||||
|
||||
const TxDetails = () => {
|
||||
const { data, isLoading, isError, socketStatus, error } = useFetchTxInfo();
|
||||
const { data, isPlaceholderData, isError, socketStatus, error } = useFetchTxInfo();
|
||||
|
||||
const [ isExpanded, setIsExpanded ] = React.useState(false);
|
||||
|
||||
@@ -62,10 +62,6 @@ const TxDetails = () => {
|
||||
}, []);
|
||||
const executionSuccessIconColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
|
||||
|
||||
if (isLoading) {
|
||||
return <TxDetailsSkeleton/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (error?.status === 422) {
|
||||
throw Error('Invalid tx hash', { cause: error as unknown as Error });
|
||||
@@ -78,6 +74,10 @@ const TxDetails = () => {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addressFromTags = [
|
||||
...data.from.private_tags || [],
|
||||
...data.from.public_tags || [],
|
||||
@@ -129,20 +129,22 @@ const TxDetails = () => {
|
||||
title="Transaction hash"
|
||||
hint="Unique character string (TxID) assigned to every verified transaction"
|
||||
flexWrap="nowrap"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
|
||||
<Box overflow="hidden">
|
||||
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
|
||||
<HashStringShortenDynamic hash={ data.hash }/>
|
||||
</Box>
|
||||
<CopyToClipboard text={ data.hash }/>
|
||||
</Skeleton>
|
||||
<CopyToClipboard text={ data.hash } isLoading={ isPlaceholderData }/>
|
||||
{ /* api doesn't support navigation between certain address account tx */ }
|
||||
{ /* <PrevNext ml={ 7 }/> */ }
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Status"
|
||||
hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<TxStatus status={ data.status } errorText={ data.status === 'error' ? data.result : undefined }/>
|
||||
<TxStatus status={ data.status } errorText={ data.status === 'error' ? data.result : undefined } isLoading={ isPlaceholderData }/>
|
||||
</DetailsInfoItem>
|
||||
{ data.revert_reason && (
|
||||
<DetailsInfoItem
|
||||
@@ -155,16 +157,22 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="Block"
|
||||
hint="Block number containing the transaction"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
{ data.block === null ?
|
||||
<Text>Pending</Text> :
|
||||
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.block) } }) }>{ data.block }</LinkInternal> }
|
||||
<Text>Pending</Text> : (
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.block) } }) }>
|
||||
{ data.block }
|
||||
</LinkInternal>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ Boolean(data.confirmations) && (
|
||||
<>
|
||||
<TextSeparator color="gray.500"/>
|
||||
<Text variant="secondary">
|
||||
{ data.confirmations } Block confirmations
|
||||
</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
|
||||
<span>{ data.confirmations } Block confirmations</span>
|
||||
</Skeleton>
|
||||
</>
|
||||
) }
|
||||
</DetailsInfoItem>
|
||||
@@ -172,12 +180,18 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="Timestamp"
|
||||
hint="Date & time of transaction inclusion, including length of time for confirmation"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
|
||||
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } boxSize={ 5 }>
|
||||
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
|
||||
</Skeleton>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Skeleton>
|
||||
<TextSeparator/>
|
||||
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }<TextSeparator color="gray.500"/></Text>
|
||||
<Text variant="secondary">{ getConfirmationDuration(data.confirmation_duration) }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }</Skeleton>
|
||||
<TextSeparator color="gray.500"/>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
|
||||
<span>{ getConfirmationDuration(data.confirmation_duration) }</span>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
<DetailsSponsoredItem/>
|
||||
@@ -194,12 +208,13 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="From"
|
||||
hint="Address (external or contract) sending the transaction"
|
||||
isLoading={ isPlaceholderData }
|
||||
columnGap={ 3 }
|
||||
>
|
||||
<Address>
|
||||
<AddressIcon address={ data.from }/>
|
||||
<AddressLink type="address" ml={ 2 } hash={ data.from.hash }/>
|
||||
<CopyToClipboard text={ data.from.hash }/>
|
||||
<AddressIcon address={ data.from } isLoading={ isPlaceholderData }/>
|
||||
<AddressLink type="address" ml={ 2 } hash={ data.from.hash } isLoading={ isPlaceholderData }/>
|
||||
<CopyToClipboard text={ data.from.hash } isLoading={ isPlaceholderData }/>
|
||||
</Address>
|
||||
{ data.from.name && <Text>{ data.from.name }</Text> }
|
||||
{ addressFromTags.length > 0 && (
|
||||
@@ -211,6 +226,7 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title={ data.to?.is_contract ? 'Interacted with contract' : 'To' }
|
||||
hint="Address (external or contract) receiving the transaction"
|
||||
isLoading={ isPlaceholderData }
|
||||
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
|
||||
columnGap={ 3 }
|
||||
>
|
||||
@@ -218,11 +234,11 @@ const TxDetails = () => {
|
||||
<>
|
||||
{ data.to && data.to.hash ? (
|
||||
<Address alignItems="center">
|
||||
<AddressIcon address={ toAddress }/>
|
||||
<AddressLink type="address" ml={ 2 } hash={ toAddress.hash }/>
|
||||
<AddressIcon address={ toAddress } isLoading={ isPlaceholderData }/>
|
||||
<AddressLink type="address" ml={ 2 } hash={ toAddress.hash } isLoading={ isPlaceholderData }/>
|
||||
{ executionSuccessBadge }
|
||||
{ executionFailedBadge }
|
||||
<CopyToClipboard text={ toAddress.hash }/>
|
||||
<CopyToClipboard text={ toAddress.hash } isLoading={ isPlaceholderData }/>
|
||||
</Address>
|
||||
) : (
|
||||
<Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre" alignItems="center">
|
||||
@@ -252,48 +268,58 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="Value"
|
||||
hint="Value sent in the native token (and USD) if applicable"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<CurrencyValue value={ data.value } currency={ appConfig.network.currency.symbol } exchangeRate={ data.exchange_rate }/>
|
||||
<CurrencyValue value={ data.value } currency={ appConfig.network.currency.symbol } exchangeRate={ data.exchange_rate } isLoading={ isPlaceholderData }/>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Transaction fee"
|
||||
hint="Total transaction fee"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<CurrencyValue
|
||||
value={ data.fee.value }
|
||||
currency={ appConfig.network.currency.symbol }
|
||||
exchangeRate={ data.exchange_rate }
|
||||
flexWrap="wrap"
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Gas price"
|
||||
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
|
||||
<Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } mr={ 1 }>
|
||||
{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
|
||||
</Skeleton>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
|
||||
<span>({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</span>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Gas usage & limit by txn"
|
||||
hint="Actual gas amount used by the transaction"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
|
||||
<TextSeparator/>
|
||||
<Text >{ BigNumber(data.gas_limit).toFormat() }</Text>
|
||||
<Utilization ml={ 4 } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>{ BigNumber(data.gas_limit).toFormat() }</Skeleton>
|
||||
<Utilization ml={ 4 } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() } isLoading={ isPlaceholderData }/>
|
||||
</DetailsInfoItem>
|
||||
{ (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && (
|
||||
<DetailsInfoItem
|
||||
title="Gas fees (Gwei)"
|
||||
// eslint-disable-next-line max-len
|
||||
hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
{ data.base_fee_per_gas && (
|
||||
<Box>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
<Text as="span" fontWeight="500">Base: </Text>
|
||||
<Text fontWeight="600" as="span">{ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
|
||||
{ (data.max_fee_per_gas || data.max_priority_fee_per_gas) && <TextSeparator/> }
|
||||
</Box>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ data.max_fee_per_gas && (
|
||||
<Box>
|
||||
@@ -330,6 +356,7 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="L1 gas used by txn"
|
||||
hint="L1 gas used by transaction"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ BigNumber(data.l1_gas_used).toFormat() }</Text>
|
||||
</DetailsInfoItem>
|
||||
@@ -338,6 +365,7 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="L1 gas price"
|
||||
hint="L1 gas price"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text mr={ 1 }>{ BigNumber(data.l1_gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
|
||||
<Text variant="secondary">({ BigNumber(data.l1_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
|
||||
@@ -348,6 +376,7 @@ const TxDetails = () => {
|
||||
title="L1 fee"
|
||||
// eslint-disable-next-line max-len
|
||||
hint={ `L1 Data Fee which is used to cover the L1 "security" cost from the batch submission mechanism. In combination with L2 execution fee, L1 fee makes the total amount of fees that a transaction pays.` }
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<CurrencyValue
|
||||
value={ data.l1_fee }
|
||||
@@ -361,6 +390,7 @@ const TxDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="L1 fee scalar"
|
||||
hint="A Dynamic overhead (fee scalar) premium, which serves as a buffer in case L1 prices rapidly increase."
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ data.l1_fee_scalar }</Text>
|
||||
</DetailsInfoItem>
|
||||
@@ -369,16 +399,17 @@ const TxDetails = () => {
|
||||
) }
|
||||
<GridItem colSpan={{ base: undefined, lg: 2 }}>
|
||||
<Element name="TxDetails__cutLink">
|
||||
<Link
|
||||
mt={ 6 }
|
||||
display="inline-block"
|
||||
fontSize="sm"
|
||||
textDecorationLine="underline"
|
||||
textDecorationStyle="dashed"
|
||||
onClick={ handleCutClick }
|
||||
>
|
||||
{ isExpanded ? 'Hide details' : 'View details' }
|
||||
</Link>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } mt={ 6 } display="inline-block">
|
||||
<Link
|
||||
display="inline-block"
|
||||
fontSize="sm"
|
||||
textDecorationLine="underline"
|
||||
textDecorationStyle="dashed"
|
||||
onClick={ handleCutClick }
|
||||
>
|
||||
{ isExpanded ? 'Hide details' : 'View details' }
|
||||
</Link>
|
||||
</Skeleton>
|
||||
</Element>
|
||||
</GridItem>
|
||||
{ isExpanded && (
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
|
||||
|
||||
const TxDetailsSkeleton = () => {
|
||||
const sectionGap = (
|
||||
<GridItem
|
||||
colSpan={{ base: undefined, lg: 2 }}
|
||||
mt={{ base: 2, lg: 3 }}
|
||||
mb={{ base: 0, lg: 3 }}
|
||||
borderBottom="1px solid"
|
||||
borderColor="divider"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px" pt={{ base: 1, lg: 2 }}>
|
||||
<DetailsSkeletonRow/>
|
||||
<DetailsSkeletonRow w="20%"/>
|
||||
<DetailsSkeletonRow w="50%"/>
|
||||
<DetailsSkeletonRow/>
|
||||
<DetailsSkeletonRow w="70%"/>
|
||||
<DetailsSkeletonRow w="70%"/>
|
||||
{ sectionGap }
|
||||
<DetailsSkeletonRow w="40%"/>
|
||||
<DetailsSkeletonRow w="40%"/>
|
||||
<DetailsSkeletonRow w="40%"/>
|
||||
<DetailsSkeletonRow w="40%"/>
|
||||
<DetailsSkeletonRow w="40%"/>
|
||||
{ sectionGap }
|
||||
<GridItem colSpan={{ base: undefined, lg: 2 }}>
|
||||
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxDetailsSkeleton;
|
||||
@@ -12,6 +12,7 @@ import delay from 'lib/delay';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import useSocketMessage from 'lib/socket/useSocketMessage';
|
||||
import { TX } from 'stubs/tx';
|
||||
|
||||
interface Params {
|
||||
onTxStatusUpdate?: () => void;
|
||||
@@ -33,6 +34,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
|
||||
queryOptions: {
|
||||
enabled: Boolean(hash),
|
||||
refetchOnMount: false,
|
||||
placeholderData: TX,
|
||||
},
|
||||
});
|
||||
const { data, isError, isLoading } = queryResult;
|
||||
|
||||
Reference in New Issue
Block a user