Commit 0e0952c0 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Graceful service degradation: block page (#1541)

* display main data

* display rest of tx fields

* placeholder data

* always show tabs

* manage error and retries on details tab

* don't retry if degraded view is active

* fix tests

* fix tx receipt

* change styles and wording for warning

* tweaks

* gray alert style change and update screenshots

* display main block info from RPC

* refetch API query

* txs tab

* display empty address for contract creation tx

* withdrawals tab

* remove type coercions

* lazy rendered lists

* fix RPC query refetch

* fixes

* comment about block type

* fix tests

* update screenshots

* [skip ci] better variable name
parent c0e27af7
...@@ -49,8 +49,9 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com ...@@ -49,8 +49,9 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_HAS_USER_OPS='true'
#meta #meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
import _clamp from 'lodash/clamp';
import React from 'react';
import { useInView } from 'react-intersection-observer';
const STEP = 10;
const MIN_ITEMS_NUM = 50;
export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boolean) {
const [ renderedItemsNum, setRenderedItemsNum ] = React.useState(MIN_ITEMS_NUM);
const { ref, inView } = useInView({
rootMargin: '200px',
triggerOnce: false,
skip: !isEnabled || list.length <= MIN_ITEMS_NUM,
});
React.useEffect(() => {
if (inView) {
setRenderedItemsNum((prev) => _clamp(prev + STEP, 0, list.length));
}
}, [ inView, list.length ]);
return { cutRef: ref, renderedItemsNum };
}
import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt } from 'viem'; import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt, Withdrawal } from 'viem';
import { ADDRESS_HASH } from './addressParams'; import { ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block'; import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
export const WITHDRAWAL: Withdrawal = {
index: '0x1af95d9',
validatorIndex: '0x7d748',
address: '0x9b52b9033ecbb6635f1c31a646d5691b282878aa',
amount: '0x29e16b',
};
export const GET_TRANSACTION: GetTransactionReturnType<Chain, 'latest'> = { export const GET_TRANSACTION: GetTransactionReturnType<Chain, 'latest'> = {
blockHash: BLOCK_HASH, blockHash: BLOCK_HASH,
blockNumber: BigInt(10361367), blockNumber: BigInt(10361367),
...@@ -70,7 +77,12 @@ export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = { ...@@ -70,7 +77,12 @@ export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = {
], ],
transactionsRoot: TX_HASH, transactionsRoot: TX_HASH,
uncles: [], uncles: [],
withdrawals: [ ], withdrawals: Array(10).fill(WITHDRAWAL),
withdrawalsRoot: TX_HASH, withdrawalsRoot: TX_HASH,
sealFields: [ '0x00' ], sealFields: [ '0x00' ],
}; };
export const GET_BLOCK_WITH_TRANSACTIONS: GetBlockReturnType<Chain, true, 'latest'> = {
...GET_BLOCK,
transactions: Array(50).fill(GET_TRANSACTION),
};
...@@ -13,7 +13,7 @@ export interface Block { ...@@ -13,7 +13,7 @@ export interface Block {
hash: string; hash: string;
parent_hash: string; parent_hash: string;
difficulty: string; difficulty: string;
total_difficulty: string; total_difficulty: string | null;
gas_used: string | null; gas_used: string | null;
gas_limit: string; gas_limit: string;
nonce: string; nonce: string;
...@@ -69,7 +69,7 @@ export type BlockWithdrawalsResponse = { ...@@ -69,7 +69,7 @@ export type BlockWithdrawalsResponse = {
next_page_params: { next_page_params: {
index: number; index: number;
items_count: number; items_count: number;
}; } | null;
} }
export type BlockWithdrawalsItem = { export type BlockWithdrawalsItem = {
......
...@@ -26,7 +26,7 @@ export type Transaction = { ...@@ -26,7 +26,7 @@ export type Transaction = {
hash: string; hash: string;
result: string; result: string;
confirmations: number; confirmations: number;
status: 'ok' | 'error' | null; status: 'ok' | 'error' | null | undefined;
block: number | null; block: number | null;
timestamp: string | null; timestamp: string | null;
confirmation_duration: Array<number> | null; confirmation_duration: Array<number> | null;
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block';
import type { ResourceError } from 'lib/api/resources';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
import BlockDetails from './BlockDetails'; import BlockDetails from './BlockDetails';
import type { BlockQuery } from './useBlockQuery';
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -22,7 +19,7 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -22,7 +19,7 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
const query = { const query = {
data: blockMock.base, data: blockMock.base,
isPending: false, isPending: false,
} as UseQueryResult<Block, ResourceError>; } as BlockQuery;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
...@@ -40,7 +37,7 @@ test('genesis block', async({ mount, page }) => { ...@@ -40,7 +37,7 @@ test('genesis block', async({ mount, page }) => {
const query = { const query = {
data: blockMock.genesis, data: blockMock.genesis,
isPending: false, isPending: false,
} as UseQueryResult<Block, ResourceError>; } as BlockQuery;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
...@@ -63,7 +60,7 @@ customFieldsTest('rootstock custom fields', async({ mount, page }) => { ...@@ -63,7 +60,7 @@ customFieldsTest('rootstock custom fields', async({ mount, page }) => {
const query = { const query = {
data: blockMock.rootstock, data: blockMock.rootstock,
isPending: false, isPending: false,
} as UseQueryResult<Block, ResourceError>; } as BlockQuery;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
......
import { Grid, GridItem, Text, Link, Box, Tooltip, useColorModeValue, Skeleton } from '@chakra-ui/react'; import { Grid, GridItem, Text, Link, Box, Tooltip, useColorModeValue, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
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 type { Block } from 'types/api/block';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward'; import getBlockReward from 'lib/block/getBlockReward';
import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts'; import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
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 DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp'; import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
...@@ -34,8 +28,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet'; ...@@ -34,8 +28,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import type { BlockQuery } from './useBlockQuery';
interface Props { interface Props {
query: UseQueryResult<Block, ResourceError>; query: BlockQuery;
} }
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled; const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
...@@ -47,7 +43,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -47,7 +43,7 @@ const BlockDetails = ({ query }: Props) => {
const separatorColor = useColorModeValue('gray.200', 'gray.700'); const separatorColor = useColorModeValue('gray.200', 'gray.700');
const { data, isPlaceholderData, isError, error } = query; const { data, isPlaceholderData } = query;
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag); setIsExpanded((flag) => !flag);
...@@ -68,14 +64,6 @@ const BlockDetails = ({ query }: Props) => { ...@@ -68,14 +64,6 @@ const BlockDetails = ({ query }: Props) => {
router.push({ pathname: '/block/[height_or_hash]', query: { height_or_hash: nextId } }, undefined); router.push({ pathname: '/block/[height_or_hash]', query: { height_or_hash: nextId } }, undefined);
}, [ data, router ]); }, [ data, router ]);
if (isError) {
if (error?.status === 404 || error?.status === 422) {
throwOnResourceLoadError({ isError, error });
}
return <DataFetchAlert/>;
}
if (!data) { if (!data) {
return null; return null;
} }
...@@ -308,7 +296,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -308,7 +296,7 @@ const BlockDetails = ({ query }: Props) => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ !config.UI.views.block.hiddenFields?.burnt_fees && ( { !config.UI.views.block.hiddenFields?.burnt_fees && !burntFees.isEqualTo(ZERO) && (
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint={ hint={
...@@ -437,6 +425,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -437,6 +425,7 @@ const BlockDetails = ({ query }: Props) => {
<HashStringShortenDynamic hash={ BigNumber(data.difficulty).toFormat() }/> <HashStringShortenDynamic hash={ BigNumber(data.difficulty).toFormat() }/>
</Box> </Box>
</DetailsInfoItem> </DetailsInfoItem>
{ data.total_difficulty && (
<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"
...@@ -445,6 +434,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -445,6 +434,7 @@ const BlockDetails = ({ query }: Props) => {
<HashStringShortenDynamic hash={ BigNumber(data.total_difficulty).toFormat() }/> <HashStringShortenDynamic hash={ BigNumber(data.total_difficulty).toFormat() }/>
</Box> </Box>
</DetailsInfoItem> </DetailsInfoItem>
) }
<DetailsInfoItemDivider/> <DetailsInfoItemDivider/>
......
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem'; import WithdrawalsList from 'ui/withdrawals/WithdrawalsList';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable'; import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
type Props = { type Props = {
...@@ -14,14 +14,11 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { ...@@ -14,14 +14,11 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
const content = blockWithdrawalsQuery.data?.items ? ( const content = blockWithdrawalsQuery.data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ blockWithdrawalsQuery.data.items.map((item, index) => ( <WithdrawalsList
<WithdrawalsListItem items={ blockWithdrawalsQuery.data.items }
key={ item.index + (blockWithdrawalsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
view="block"
isLoading={ blockWithdrawalsQuery.isPlaceholderData } isLoading={ blockWithdrawalsQuery.isPlaceholderData }
view="block"
/> />
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<WithdrawalsTable <WithdrawalsTable
......
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Chain, GetBlockReturnType } from 'viem';
import type { Block } from 'types/api/block';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import { publicClient } from 'lib/web3/client';
import { BLOCK } from 'stubs/block';
import { GET_BLOCK } from 'stubs/RPC';
import { unknownAddress } from 'ui/shared/address/utils';
type RpcResponseType = GetBlockReturnType<Chain, false, 'latest'> | null;
export type BlockQuery = UseQueryResult<Block, ResourceError<{ status: number }>> & {
isDegradedData: boolean;
};
interface Params {
heightOrHash: string;
}
export default function useBlockQuery({ heightOrHash }: Params): BlockQuery {
const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false);
const apiQuery = useApiQuery<'block', { status: number }>('block', {
pathParams: { height_or_hash: heightOrHash },
queryOptions: {
enabled: Boolean(heightOrHash),
placeholderData: BLOCK,
refetchOnMount: false,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
}
return retry(failureCount, error);
},
refetchInterval: (): number | false => {
return isRefetchEnabled ? 15 * SECOND : false;
},
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, Block | null>({
queryKey: [ 'RPC', 'block', { heightOrHash } ],
queryFn: async() => {
const blockParams = heightOrHash.startsWith('0x') ? { blockHash: heightOrHash as `0x${ string }` } : { blockNumber: BigInt(heightOrHash) };
return publicClient.getBlock(blockParams).catch(() => null);
},
select: (block) => {
if (!block) {
return null;
}
return {
height: Number(block.number),
timestamp: dayjs.unix(Number(block.timestamp)).format(),
tx_count: block.transactions.length,
miner: { ...unknownAddress, hash: block.miner },
size: Number(block.size),
hash: block.hash,
parent_hash: block.parentHash,
difficulty: block.difficulty.toString(),
total_difficulty: block.totalDifficulty?.toString() ?? null,
gas_used: block.gasUsed.toString(),
gas_limit: block.gasLimit.toString(),
nonce: block.nonce,
base_fee_per_gas: block.baseFeePerGas?.toString() ?? null,
burnt_fees: null,
priority_fee: null,
extra_data: block.extraData,
state_root: block.stateRoot,
gas_target_percentage: null,
gas_used_percentage: null,
burnt_fees_percentage: null,
type: 'block', // we can't get this type from RPC, so it will always be a regular block
tx_fees: null,
uncles_hashes: block.uncles,
withdrawals_count: block.withdrawals?.length,
};
},
placeholderData: GET_BLOCK,
enabled: apiQuery.isError || apiQuery.errorUpdateCount > 0,
retry: false,
refetchOnMount: false,
});
React.useEffect(() => {
if (apiQuery.isPlaceholderData) {
return;
}
if (apiQuery.isError && apiQuery.errorUpdateCount === 1) {
setRefetchEnabled(true);
} else if (!apiQuery.isError) {
setRefetchEnabled(false);
}
}, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]);
React.useEffect(() => {
if (!rpcQuery.isPlaceholderData && !rpcQuery.data) {
setRefetchEnabled(false);
}
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0 && rpcQuery.data);
const query = isRpcQuery ? rpcQuery as UseQueryResult<Block, ResourceError<{ status: number }>> : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Chain, GetBlockReturnType } from 'viem';
import type { BlockTransactionsResponse } from 'types/api/block';
import type { ResourceError } from 'lib/api/resources';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import hexToDecimal from 'lib/hexToDecimal';
import { publicClient } from 'lib/web3/client';
import { GET_BLOCK_WITH_TRANSACTIONS } from 'stubs/RPC';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { unknownAddress } from 'ui/shared/address/utils';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import { emptyPagination } from 'ui/shared/pagination/utils';
import type { BlockQuery } from './useBlockQuery';
type RpcResponseType = GetBlockReturnType<Chain, boolean, 'latest'> | null;
export type BlockTxsQuery = QueryWithPagesResult<'block_txs'> & {
isDegradedData: boolean;
};
interface Params {
heightOrHash: string;
blockQuery: BlockQuery;
tab: string;
}
export default function useBlockTxQuery({ heightOrHash, blockQuery, tab }: Params): BlockTxsQuery {
const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false);
const apiQuery = useQueryWithPages({
resourceName: 'block_txs',
pathParams: { height_or_hash: heightOrHash },
options: {
enabled: Boolean(tab === 'txs' && !blockQuery.isPlaceholderData && !blockQuery.isDegradedData),
placeholderData: generateListStub<'block_txs'>(TX, 50, { next_page_params: {
block_number: 9004925,
index: 49,
items_count: 50,
} }),
refetchOnMount: false,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
}
return retry(failureCount, error);
},
refetchInterval: (): number | false => {
return isRefetchEnabled ? 15 * SECOND : false;
},
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, BlockTransactionsResponse | null>({
queryKey: [ 'RPC', 'block_txs', { heightOrHash } ],
queryFn: async() => {
const blockParams = heightOrHash.startsWith('0x') ?
{ blockHash: heightOrHash as `0x${ string }`, includeTransactions: true } :
{ blockNumber: BigInt(heightOrHash), includeTransactions: true };
return publicClient.getBlock(blockParams).catch(() => null);
},
select: (block) => {
if (!block) {
return null;
}
return {
items: block.transactions
.map((tx) => {
if (typeof tx === 'string') {
return;
}
return {
from: { ...unknownAddress, hash: tx.from as string },
to: tx.to ? { ...unknownAddress, hash: tx.to as string } : null,
hash: tx.hash as string,
timestamp: block?.timestamp ? dayjs.unix(Number(block.timestamp)).format() : null,
confirmation_duration: null,
status: undefined,
block: Number(block.number),
value: tx.value.toString(),
gas_price: tx.gasPrice?.toString() ?? null,
base_fee_per_gas: block?.baseFeePerGas?.toString() ?? null,
max_fee_per_gas: tx.maxFeePerGas?.toString() ?? null,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas?.toString() ?? null,
nonce: tx.nonce,
position: tx.transactionIndex,
type: tx.typeHex ? hexToDecimal(tx.typeHex) : null,
raw_input: tx.input,
gas_used: null,
gas_limit: tx.gas.toString(),
confirmations: 0,
fee: {
value: null,
type: 'actual',
},
created_contract: null,
result: '',
priority_fee: null,
tx_burnt_fee: null,
revert_reason: null,
decoded_input: null,
has_error_in_internal_txs: null,
token_transfers: null,
token_transfers_overflow: false,
exchange_rate: null,
method: null,
tx_types: [],
tx_tag: null,
actions: [],
};
})
.filter(Boolean),
next_page_params: null,
};
},
placeholderData: GET_BLOCK_WITH_TRANSACTIONS,
enabled: tab === 'txs' && (blockQuery.isDegradedData || apiQuery.isError || apiQuery.errorUpdateCount > 0),
retry: false,
refetchOnMount: false,
});
React.useEffect(() => {
if (apiQuery.isPlaceholderData) {
return;
}
if (apiQuery.isError && apiQuery.errorUpdateCount === 1) {
setRefetchEnabled(true);
} else if (!apiQuery.isError) {
setRefetchEnabled(false);
}
}, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]);
React.useEffect(() => {
if (!rpcQuery.isPlaceholderData && !rpcQuery.data) {
setRefetchEnabled(false);
}
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean((
blockQuery.isDegradedData ||
((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0)
) && rpcQuery.data);
const rpcQueryWithPages: QueryWithPagesResult<'block_txs'> = React.useMemo(() => {
return {
...rpcQuery as UseQueryResult<BlockTransactionsResponse, ResourceError>,
pagination: emptyPagination,
onFilterChange: () => {},
onSortingChange: () => {},
};
}, [ rpcQuery ]);
const query = isRpcQuery ? rpcQueryWithPages : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Chain, GetBlockReturnType } from 'viem';
import type { BlockWithdrawalsResponse } from 'types/api/block';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import hexToDecimal from 'lib/hexToDecimal';
import { publicClient } from 'lib/web3/client';
import { GET_BLOCK } from 'stubs/RPC';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import { unknownAddress } from 'ui/shared/address/utils';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import { emptyPagination } from 'ui/shared/pagination/utils';
import type { BlockQuery } from './useBlockQuery';
type RpcResponseType = GetBlockReturnType<Chain, false, 'latest'> | null;
export type BlockWithdrawalsQuery = QueryWithPagesResult<'block_withdrawals'> & {
isDegradedData: boolean;
};
interface Params {
heightOrHash: string;
blockQuery: BlockQuery;
tab: string;
}
export default function useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab }: Params): BlockWithdrawalsQuery {
const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false);
const apiQuery = useQueryWithPages({
resourceName: 'block_withdrawals',
pathParams: { height_or_hash: heightOrHash },
options: {
enabled:
tab === 'withdrawals' &&
config.features.beaconChain.isEnabled &&
!blockQuery.isPlaceholderData && !blockQuery.isDegradedData,
placeholderData: generateListStub<'block_withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5,
items_count: 50,
} }),
refetchOnMount: false,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
}
return retry(failureCount, error);
},
refetchInterval: (): number | false => {
return isRefetchEnabled ? 15 * SECOND : false;
},
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, BlockWithdrawalsResponse | null>({
queryKey: [ 'RPC', 'block', { heightOrHash } ],
queryFn: async() => {
const blockParams = heightOrHash.startsWith('0x') ? { blockHash: heightOrHash as `0x${ string }` } : { blockNumber: BigInt(heightOrHash) };
return publicClient.getBlock(blockParams).catch(() => null);
},
select: (block) => {
if (!block) {
return null;
}
return {
items: block.withdrawals
?.map((withdrawal) => {
return {
amount: hexToDecimal(withdrawal.amount).toString(),
index: hexToDecimal(withdrawal.index),
validator_index: hexToDecimal(withdrawal.validatorIndex),
receiver: { ...unknownAddress, hash: withdrawal.address },
};
})
.sort((a, b) => b.index - a.index) ?? [],
next_page_params: null,
};
},
placeholderData: GET_BLOCK,
enabled:
tab === 'withdrawals' &&
config.features.beaconChain.isEnabled &&
(blockQuery.isDegradedData || apiQuery.isError || apiQuery.errorUpdateCount > 0),
retry: false,
refetchOnMount: false,
});
React.useEffect(() => {
if (apiQuery.isPlaceholderData) {
return;
}
if (apiQuery.isError && apiQuery.errorUpdateCount === 1) {
setRefetchEnabled(true);
} else if (!apiQuery.isError) {
setRefetchEnabled(false);
}
}, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]);
React.useEffect(() => {
if (!rpcQuery.isPlaceholderData && !rpcQuery.data) {
setRefetchEnabled(false);
}
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean((
blockQuery.isDegradedData ||
((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0)
) && rpcQuery.data);
const rpcQueryWithPages: QueryWithPagesResult<'block_withdrawals'> = React.useMemo(() => {
return {
...rpcQuery as UseQueryResult<BlockWithdrawalsResponse, ResourceError>,
pagination: emptyPagination,
onFilterChange: () => {},
onSortingChange: () => {},
};
}, [ rpcQuery ]);
const query = isRpcQuery ? rpcQueryWithPages : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
...@@ -6,24 +6,22 @@ import type { PaginationParams } from 'ui/shared/pagination/types'; ...@@ -6,24 +6,22 @@ import type { PaginationParams } from 'ui/shared/pagination/types';
import type { RoutedTab } from 'ui/shared/Tabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError'; import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import BlockWithdrawals from 'ui/block/BlockWithdrawals'; import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import useBlockQuery from 'ui/block/useBlockQuery';
import useBlockTxQuery from 'ui/block/useBlockTxQuery';
import useBlockWithdrawalsQuery from 'ui/block/useBlockWithdrawalsQuery';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
...@@ -41,45 +39,42 @@ const BlockPageContent = () => { ...@@ -41,45 +39,42 @@ const BlockPageContent = () => {
const heightOrHash = getQueryParamString(router.query.height_or_hash); const heightOrHash = getQueryParamString(router.query.height_or_hash);
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
const blockQuery = useApiQuery('block', { const blockQuery = useBlockQuery({ heightOrHash });
pathParams: { height_or_hash: heightOrHash }, const blockTxsQuery = useBlockTxQuery({ heightOrHash, blockQuery, tab });
queryOptions: { const blockWithdrawalsQuery = useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab });
enabled: Boolean(heightOrHash),
placeholderData: BLOCK,
},
});
const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs',
pathParams: { height_or_hash: heightOrHash },
options: {
enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && tab === 'txs'),
placeholderData: generateListStub<'block_txs'>(TX, 50, { next_page_params: {
block_number: 9004925,
index: 49,
items_count: 50,
} }),
},
});
const blockWithdrawalsQuery = useQueryWithPages({
resourceName: 'block_withdrawals',
pathParams: { height_or_hash: heightOrHash },
options: {
enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && config.features.beaconChain.isEnabled && tab === 'withdrawals'),
placeholderData: generateListStub<'block_withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5,
items_count: 50,
} }),
},
});
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> }, {
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, id: 'index',
title: 'Details',
component: (
<>
{ blockQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockQuery.isPlaceholderData } mb={ 6 }/> }
<BlockDetails query={ blockQuery }/>
</>
),
},
{
id: 'txs',
title: 'Transactions',
component: (
<>
{ blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> }
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/>
</>
),
},
config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ? config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ?
{ id: 'withdrawals', title: 'Withdrawals', component: <BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/> } : {
null, id: 'withdrawals',
title: 'Withdrawals',
component: (
<>
{ blockWithdrawalsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockWithdrawalsQuery.isPlaceholderData } mb={ 6 }/> }
<BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/>
</>
),
} : null,
].filter(Boolean)), [ blockQuery, blockTxsQuery, blockWithdrawalsQuery ]); ].filter(Boolean)), [ blockQuery, blockTxsQuery, blockWithdrawalsQuery ]);
const hasPagination = !isMobile && ( const hasPagination = !isMobile && (
......
...@@ -57,7 +57,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading ...@@ -57,7 +57,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
w="min-content" w="min-content"
/> />
</Flex> </Flex>
{ to ? ( { to && (
<Entity <Entity
address={ to } address={ to }
isLoading={ isLoading } isLoading={ isLoading }
...@@ -70,7 +70,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading ...@@ -70,7 +70,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
w="min-content" w="min-content"
ml="28px" ml="28px"
/> />
) : <span>-</span> } ) }
</Flex> </Flex>
); );
} }
...@@ -95,7 +95,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading ...@@ -95,7 +95,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
isLoading={ isLoading } isLoading={ isLoading }
type={ getTxCourseType(from.hash, to?.hash, current) } type={ getTxCourseType(from.hash, to?.hash, current) }
/> />
{ to ? ( { to && (
<Entity <Entity
address={ to } address={ to }
isLoading={ isLoading } isLoading={ isLoading }
...@@ -107,7 +107,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading ...@@ -107,7 +107,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
maxW={ truncation === 'constant' ? undefined : `calc(50% - ${ iconSizeWithMargins / 2 }px)` } maxW={ truncation === 'constant' ? undefined : `calc(50% - ${ iconSizeWithMargins / 2 }px)` }
ml={ 3 } ml={ 3 }
/> />
) : <span>-</span> } ) }
</Flex> </Flex>
); );
}; };
......
import type { AddressParam } from 'types/api/addressParams';
export type TxCourseType = 'in' | 'out' | 'self' | 'unspecified'; export type TxCourseType = 'in' | 'out' | 'self' | 'unspecified';
export function getTxCourseType(from: string, to: string | undefined, current?: string): TxCourseType { export function getTxCourseType(from: string, to: string | undefined, current?: string): TxCourseType {
...@@ -19,3 +21,14 @@ export function getTxCourseType(from: string, to: string | undefined, current?: ...@@ -19,3 +21,14 @@ export function getTxCourseType(from: string, to: string | undefined, current?:
return 'unspecified'; return 'unspecified';
} }
export const unknownAddress: Omit<AddressParam, 'hash'> = {
is_contract: false,
is_verified: false,
implementation_name: '',
name: '',
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
};
import type { PaginationParams } from './types';
export const emptyPagination: PaginationParams = {
page: 1,
onNextPageClick: () => {},
onPrevPageClick: () => {},
resetPage: () => {},
hasPages: false,
hasNextPage: false,
canGoBackwards: false,
isLoading: false,
isVisible: false,
};
...@@ -12,6 +12,10 @@ export interface Props { ...@@ -12,6 +12,10 @@ export interface Props {
} }
const TxStatus = ({ status, errorText, isLoading }: Props) => { const TxStatus = ({ status, errorText, isLoading }: Props) => {
if (status === undefined) {
return null;
}
let text; let text;
let type: StatusTagType; let type: StatusTagType;
......
...@@ -37,6 +37,7 @@ const TxDetailsDegraded = ({ hash, txQuery }: Props) => { ...@@ -37,6 +37,7 @@ const TxDetailsDegraded = ({ hash, txQuery }: Props) => {
queryKey: [ 'RPC', 'tx', { hash } ], queryKey: [ 'RPC', 'tx', { hash } ],
queryFn: async() => { queryFn: async() => {
const tx = await publicClient.getTransaction({ hash: hash as `0x${ string }` }); const tx = await publicClient.getTransaction({ hash: hash as `0x${ string }` });
if (!tx) { if (!tx) {
throw new Error('Not found'); throw new Error('Not found');
} }
......
import { Box, Show, Hide } from '@chakra-ui/react'; import { Show, Hide } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
...@@ -8,11 +8,10 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -8,11 +8,10 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import getNextSortValue from 'ui/shared/sort/getNextSortValue'; import getNextSortValue from 'ui/shared/sort/getNextSortValue';
import TxsHeaderMobile from './TxsHeaderMobile'; import TxsHeaderMobile from './TxsHeaderMobile';
import TxsListItem from './TxsListItem'; import TxsList from './TxsList';
import TxsTable from './TxsTable'; import TxsTable from './TxsTable';
const SORT_SEQUENCE: Record<TransactionsSortingField, Array<TransactionsSortingValue | undefined>> = { const SORT_SEQUENCE: Record<TransactionsSortingField, Array<TransactionsSortingValue | undefined>> = {
...@@ -66,26 +65,16 @@ const TxsContent = ({ ...@@ -66,26 +65,16 @@ const TxsContent = ({
const content = items ? ( const content = items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<Box> <TxsList
{ showSocketInfo && (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ socketInfoNum }
alert={ socketInfoAlert }
isLoading={ isPlaceholderData }
/>
) }
{ items.map((tx, index) => (
<TxsListItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress } showSocketInfo={ showSocketInfo }
enableTimeIncrement={ enableTimeIncrement } socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
enableTimeIncrement={ enableTimeIncrement }
currentAddress={ currentAddress }
items={ items }
/> />
)) }
</Box>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TxsTable <TxsTable
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsListItem from './TxsListItem';
interface Props {
showBlockInfo: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
enableTimeIncrement?: boolean;
currentAddress?: string;
isLoading: boolean;
items: Array<Transaction>;
}
const TxsList = (props: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(props.items, !props.isLoading);
return (
<Box>
{ props.showSocketInfo && (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ props.socketInfoNum }
alert={ props.socketInfoAlert }
isLoading={ props.isLoading }
/>
) }
{ props.items.slice(0, renderedItemsNum).map((tx, index) => (
<TxsListItem
key={ tx.hash + (props.isLoading ? index : '') }
tx={ tx }
showBlockInfo={ props.showBlockInfo }
currentAddress={ props.currentAddress }
enableTimeIncrement={ props.enableTimeIncrement }
isLoading={ props.isLoading }
/>
)) }
<Box ref={ cutRef } h={ 0 }/>
</Box>
);
};
export default React.memo(TxsList);
...@@ -6,6 +6,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue } ...@@ -6,6 +6,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue }
import config from 'configs/app'; import config from 'configs/app';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
...@@ -40,6 +41,8 @@ const TxsTable = ({ ...@@ -40,6 +41,8 @@ const TxsTable = ({
enableTimeIncrement, enableTimeIncrement,
isLoading, isLoading,
}: Props) => { }: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(txs, !isLoading);
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
...@@ -81,7 +84,7 @@ const TxsTable = ({ ...@@ -81,7 +84,7 @@ const TxsTable = ({
/> />
) } ) }
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ txs.map((item, index) => ( { txs.slice(0, renderedItemsNum).map((item, index) => (
<TxsTableItem <TxsTableItem
key={ item.hash + (isLoading ? index : '') } key={ item.hash + (isLoading ? index : '') }
tx={ item } tx={ item }
...@@ -94,6 +97,7 @@ const TxsTable = ({ ...@@ -94,6 +97,7 @@ const TxsTable = ({
</AnimatePresence> </AnimatePresence>
</Tbody> </Tbody>
</Table> </Table>
<div ref={ cutRef }/>
</AddressHighlightProvider> </AddressHighlightProvider>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { AddressWithdrawalsItem } from 'types/api/address';
import type { BlockWithdrawalsItem } from 'types/api/block';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import WithdrawalsListItem from './WithdrawalsListItem';
type Props = {
isLoading?: boolean;
} & ({
items: Array<WithdrawalsItem>;
view: 'list';
} | {
items: Array<AddressWithdrawalsItem>;
view: 'address';
} | {
items: Array<BlockWithdrawalsItem>;
view: 'block';
});
const WithdrawalsList = ({ items, view, isLoading }: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(items, !isLoading);
return (
<Box>
{ items.slice(0, renderedItemsNum).map((item, index) => {
const key = item.index + (isLoading ? String(index) : '');
switch (view) {
case 'address': {
return (
<WithdrawalsListItem
key={ key }
item={ item as AddressWithdrawalsItem }
view={ view }
isLoading={ isLoading }
/>
);
}
case 'block': {
return (
<WithdrawalsListItem
key={ key }
item={ item as BlockWithdrawalsItem }
view={ view }
isLoading={ isLoading }
/>
);
}
case 'list': {
return (
<WithdrawalsListItem
key={ key }
item={ item as WithdrawalsItem }
view={ view }
isLoading={ isLoading }
/>
);
}
}
}) }
<div ref={ cutRef }/>
</Box>
);
};
export default React.memo(WithdrawalsList);
...@@ -6,6 +6,7 @@ import type { BlockWithdrawalsItem } from 'types/api/block'; ...@@ -6,6 +6,7 @@ import type { BlockWithdrawalsItem } from 'types/api/block';
import type { WithdrawalsItem } from 'types/api/withdrawals'; import type { WithdrawalsItem } from 'types/api/withdrawals';
import config from 'configs/app'; import config from 'configs/app';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem'; import WithdrawalsTableItem from './WithdrawalsTableItem';
...@@ -26,7 +27,9 @@ const feature = config.features.beaconChain; ...@@ -26,7 +27,9 @@ const feature = config.features.beaconChain;
view: 'block'; view: 'block';
}); });
const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => { const WithdrawalsTable = ({ items, isLoading, top, view }: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(items, !isLoading);
if (!feature.isEnabled) { if (!feature.isEnabled) {
return null; return null;
} }
...@@ -44,15 +47,16 @@ const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => { ...@@ -44,15 +47,16 @@ const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ view === 'list' && (items as Array<WithdrawalsItem>).map((item, index) => ( { view === 'list' && (items as Array<WithdrawalsItem>).slice(0, renderedItemsNum).map((item, index) => (
<WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="list" isLoading={ isLoading }/> <WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="list" isLoading={ isLoading }/>
)) } )) }
{ view === 'address' && (items as Array<AddressWithdrawalsItem>).map((item, index) => ( { view === 'address' && (items as Array<AddressWithdrawalsItem>).slice(0, renderedItemsNum).map((item, index) => (
<WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="address" isLoading={ isLoading }/> <WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="address" isLoading={ isLoading }/>
)) } )) }
{ view === 'block' && (items as Array<BlockWithdrawalsItem>).map((item, index) => ( { view === 'block' && (items as Array<BlockWithdrawalsItem>).slice(0, renderedItemsNum).map((item, index) => (
<WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="block" isLoading={ isLoading }/> <WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="block" isLoading={ isLoading }/>
)) } )) }
<tr ref={ cutRef }/>
</Tbody> </Tbody>
</Table> </Table>
); );
......
...@@ -10,16 +10,16 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -10,16 +10,16 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
type Props = ({ type Props = ({
item: WithdrawalsItem; item: WithdrawalsItem;
view: 'list'; view: 'list';
} | { } | {
item: AddressWithdrawalsItem; item: AddressWithdrawalsItem;
view: 'address'; view: 'address';
} | { } | {
item: BlockWithdrawalsItem; item: BlockWithdrawalsItem;
view: 'block'; view: 'block';
}) & { isLoading?: boolean }; }) & { isLoading?: boolean };
const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => { const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => {
return ( return (
......
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