Commit 7ad5a392 authored by isstuev's avatar isstuev Committed by isstuev

batch page and latest batch

parent b9230bfe
...@@ -44,4 +44,4 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co ...@@ -44,4 +44,4 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup # rollup
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=true NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=http://65.109.173.70:81/ NEXT_PUBLIC_L1_BASE_URL=http://65.109.173.70:81
...@@ -59,7 +59,7 @@ import type { TTxsFilters } from 'types/api/txsFilters'; ...@@ -59,7 +59,7 @@ import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvml2TxnBatches'; import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvml2TxnBatches';
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
import config from 'configs/app'; import config from 'configs/app';
...@@ -433,6 +433,9 @@ export const RESOURCES = { ...@@ -433,6 +433,9 @@ export const RESOURCES = {
homepage_indexing_status: { homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status', path: '/api/v2/main-page/indexing-status',
}, },
homepage_zkevm_latest_batch: {
path: '/api/v2/main-page/zkevm/batches/latest-number',
},
// SEARCH // SEARCH
quick_search: { quick_search: {
...@@ -493,6 +496,11 @@ export const RESOURCES = { ...@@ -493,6 +496,11 @@ export const RESOURCES = {
path: '/api/v2/zkevm/batches/count', path: '/api/v2/zkevm/batches/count',
}, },
zkevm_l2_txn_batch: {
path: '/api/v2/zkevm/batches/:number',
pathParams: [ 'number' as const ],
},
// CONFIGS // CONFIGS
config_backend_version: { config_backend_version: {
path: '/api/v2/config/backend-version', path: '/api/v2/config/backend-version',
...@@ -562,7 +570,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -562,7 +570,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals'; 'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -587,6 +595,7 @@ Q extends 'homepage_txs' ? Array<Transaction> : ...@@ -587,6 +595,7 @@ Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> : Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<L2DepositsItem> : Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'homepage_zkevm_latest_batch' ? number :
Q extends 'stats_counters' ? Counters : Q extends 'stats_counters' ? Counters :
Q extends 'stats_lines' ? StatsCharts : Q extends 'stats_lines' ? StatsCharts :
Q extends 'stats_line' ? StatsChart : Q extends 'stats_line' ? StatsChart :
...@@ -653,6 +662,7 @@ Q extends 'l2_deposits_count' ? number : ...@@ -653,6 +662,7 @@ Q extends 'l2_deposits_count' ? number :
Q extends 'l2_txn_batches_count' ? number : Q extends 'l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse : Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse :
Q extends 'zkevm_l2_txn_batches_count' ? number : Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'config_backend_version' ? BackendVersionConfig : Q extends 'config_backend_version' ? BackendVersionConfig :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
import type { ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvml2TxnBatches';
export const txnBatchesData: ZkEvmL2TxnBatchesResponse = {
items: [
{
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Finalized',
verify_tx_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
sequence_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b',
number: 5218590,
tx_count: 9,
},
{
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Unfinalized',
verify_tx_hash: null,
sequence_tx_hash: null,
number: 5218590,
tx_count: 9,
},
],
next_page_params: {
number: 5902834,
items_count: 50,
},
};
...@@ -8,6 +8,7 @@ export type Props = { ...@@ -8,6 +8,7 @@ export type Props = {
id: string; id: string;
height_or_hash: string; height_or_hash: string;
hash: string; hash: string;
number: string;
q: string; q: string;
} }
...@@ -19,6 +20,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => { ...@@ -19,6 +20,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
id: query.id?.toString() || '', id: query.id?.toString() || '',
hash: query.hash?.toString() || '', hash: query.hash?.toString() || '',
height_or_hash: query.height_or_hash?.toString() || '', height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '',
q: query.q?.toString() || '', q: query.q?.toString() || '',
}, },
}; };
......
...@@ -47,6 +47,7 @@ declare module "nextjs-routes" { ...@@ -47,6 +47,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/verified-contracts"> | StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml"> | StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals"> | StaticRoute<"/withdrawals">
| DynamicRoute<"/zkevm-l2-txn-batch/[number]", { "number": string }>
| StaticRoute<"/zkevm-l2-txn-batches">; | StaticRoute<"/zkevm-l2-txn-batches">;
interface StaticRoute<Pathname> { interface StaticRoute<Pathname> {
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
const ZkEvmL2TxnBatch = dynamic(() => import('ui/pages/ZkEvmL2TxnBatch'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/zkevm-l2-txn-batch/[number]" query={ props }>
<ZkEvmL2TxnBatch/>
</PageNextJs>
);
};
export default Page;
export { zkEvmL2 as getServerSideProps } from 'nextjs/getServerSideProps';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvml2TxnBatches'; import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem } from 'types/api/zkEvml2TxnBatches';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
...@@ -10,3 +10,15 @@ export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = { ...@@ -10,3 +10,15 @@ export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = {
number: 5218590, number: 5218590,
tx_count: 9, tx_count: 9,
}; };
export const ZKEVM_L2_TXN_BATCH: ZkEvmL2TxnBatch = {
acc_input_hash: '0xb815fe2832977f1324ad0124a019b938f189f7b470292f40a21284f15774b3b3',
global_exit_root: '0x0000000000000000000000000000000000000000000000000000000000000000',
number: 1,
sequence_tx_hash: '0x57b9b95db5f94f125710bdc8fbb3fabaac10125b44b0cb61dbc69daddf06d0cd',
state_root: '0xb9a589d6b3ae44d3b250a9993caa5e3721568197f56e4743989ecb2285d80ec4',
status: 'Finalized',
timestamp: '2023-09-15T06:22:48.000000Z',
transactions: [ '0xff99dd67646b8f3d657cc6f19eb33abc346de2dbaccd03e45e7726cc28e3e186' ],
verify_tx_hash: '0x093276fa65c67d7b12dd96f4fefafba9d9ad2f1c23c6e53f96583971ce75352d',
};
...@@ -65,6 +65,11 @@ export type Transaction = { ...@@ -65,6 +65,11 @@ export type Transaction = {
validator_address: AddressParam; validator_address: AddressParam;
validator_fee: string; validator_fee: string;
}; };
// zkEvm fields
zkevm_verify_hash?: string;
zkevm_batch_number?: number;
zkevm_status?: string;
zkevm_sequence_hash?: string;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
export type ZkEvmL2TxnBatchesItem = { export type ZkEvmL2TxnBatchesItem = {
number: number; number: number;
verify_tx_hash: string; verify_tx_hash: string | null;
sequence_tx_hash: string; sequence_tx_hash: string | null;
status: string; status: string;
timestamp: string; timestamp: string;
tx_count: number; tx_count: number;
...@@ -14,3 +14,15 @@ export type ZkEvmL2TxnBatchesResponse = { ...@@ -14,3 +14,15 @@ export type ZkEvmL2TxnBatchesResponse = {
items_count: number; items_count: number;
} | null; } | null;
} }
export type ZkEvmL2TxnBatch = {
acc_input_hash: string;
global_exit_root: string;
number: number;
sequence_tx_hash: string;
state_root: string;
status: string;
timestamp: string;
transactions: Array<string>;
verify_tx_hash: string;
}
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = buildApiUrl('homepage_stats');
const BLOCKS_API_URL = buildApiUrl('homepage_blocks');
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
const testL2 = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
});
testL2('L2 view', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
const testNoReward = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any,
});
testNoReward('no reward view', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with long block height', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
{
...blockMock.base,
height: 123456789012345,
},
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
test('new item', async({ mount, page, createSocket }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestBlocks/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
});
import { Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { HOMEPAGE_STATS } from 'stubs/stats';
import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem';
const LatestBlocks = () => {
const isMobile = useIsMobile();
// const blocksMaxCount = isMobile ? 2 : 3;
let blocksMaxCount: number;
if (config.features.rollup.isEnabled || config.UI.views.block.hiddenFields?.total_reward) {
blocksMaxCount = isMobile ? 4 : 5;
} else {
blocksMaxCount = isMobile ? 2 : 3;
}
const { data, isPlaceholderData, isError } = useApiQuery('homepage_blocks', {
queryOptions: {
placeholderData: Array(blocksMaxCount).fill(BLOCK),
},
});
const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => {
const newData = prevData ? [ ...prevData ] : [];
if (newData.some((block => block.height === payload.block.height))) {
return newData;
}
return [ payload.block, ...newData ].sort((b1, b2) => b2.height - b1.height).slice(0, blocksMaxCount);
});
}, [ queryClient, blocksMaxCount ]);
const channel = useSocketChannel({
topic: 'blocks:new_block',
isDisabled: isPlaceholderData || isError,
});
useSocketMessage({
channel,
event: 'new_block',
handler: handleNewBlockMessage,
});
let content;
if (isError) {
content = <Text>No data. Please reload page.</Text>;
}
if (data) {
const dataToShow = data.slice(0, blocksMaxCount);
content = (
<>
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Skeleton isLoaded={ !statsQueryResult.isPlaceholderData } mb={{ base: 6, lg: 3 }} display="inline-block">
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text>
</Skeleton>
) }
<VStack spacing={ 3 } mb={ 4 } overflow="hidden" alignItems="stretch">
<AnimatePresence initial={ false } >
{ dataToShow.map(((block, index) => (
<LatestBlocksItem
key={ block.height + (isPlaceholderData ? String(index) : '') }
block={ block }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ route({ pathname: '/blocks' }) }>View all blocks</LinkInternal>
</Flex>
</>
);
}
return (
<Box width={{ base: '100%', lg: '280px' }} flexShrink={ 0 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest blocks</Heading>
{ content }
</Box>
);
};
export default LatestBlocks;
...@@ -10,6 +10,7 @@ import clockIcon from 'icons/clock-light.svg'; ...@@ -10,6 +10,7 @@ import clockIcon from 'icons/clock-light.svg';
import bitcoinIcon from 'icons/coins/bitcoin.svg'; import bitcoinIcon from 'icons/coins/bitcoin.svg';
import gasIcon from 'icons/gas.svg'; import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import batchesIcon from 'icons/txn_batches.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
...@@ -28,7 +29,14 @@ const Stats = () => { ...@@ -28,7 +29,14 @@ const Stats = () => {
}, },
}); });
if (isError) { const zkEvmLatestBatchQuery = useApiQuery('homepage_zkevm_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: config.features.zkEvmRollup.isEnabled,
},
});
if (isError || zkEvmLatestBatchQuery.isError) {
return null; return null;
} }
...@@ -48,13 +56,23 @@ const Stats = () => { ...@@ -48,13 +56,23 @@ const Stats = () => {
content = ( content = (
<> <>
<StatsItem { config.features.zkEvmRollup.isEnabled ? (
icon={ blockIcon } <StatsItem
title="Total blocks" icon={ batchesIcon }
value={ Number(data.total_blocks).toLocaleString() } title="Latest batch "
url={ route({ pathname: '/blocks' }) } value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
isLoading={ isPlaceholderData } url={ route({ pathname: '/zkevm-l2-txn-batches' }) }
/> isLoading={ zkEvmLatestBatchQuery.isPlaceholderData }
/>
) : (
<StatsItem
icon={ blockIcon }
title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) }
isLoading={ isPlaceholderData }
/>
) }
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsItem
icon={ clockIcon } icon={ clockIcon }
......
import { useRouter } from 'next/router';
import React from 'react';
// import type { PaginationParams } from 'ui/shared/pagination/types';
// import type { RoutedTab } from 'ui/shared/Tabs/types';
// import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
// import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ZKEVM_L2_TXN_BATCH } from 'stubs/zkEvmL2';
// import { TX } from 'stubs/tx';
// import { generateListStub } from 'stubs/utils';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import ZkEvmL2TxnBatchDetails from 'ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails';
// import Pagination from 'ui/shared/pagination/Pagination';
// import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
// import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
// import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
// import TxsContent from 'ui/txs/TxsContent';
// const TAB_LIST_PROPS = {
// marginBottom: 0,
// py: 5,
// marginTop: -5,
// };
const ZkEvmL2TxnBatch = () => {
const router = useRouter();
// const isMobile = useIsMobile();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
// const tab = getQueryParamString(router.query.tab);
const batchQuery = useApiQuery('zkevm_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: ZKEVM_L2_TXN_BATCH,
},
});
// const blockTxsQuery = useQueryWithPages({
// resourceName: 'batch_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,
// } }),
// },
// });
if (!number) {
throw new Error('Tx batch not found', { cause: { status: 404 } });
}
if (batchQuery.isError) {
throw new Error(undefined, { cause: batchQuery.error });
}
// const tabs: Array<RoutedTab> = React.useMemo(() => ([
// { id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
// { id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
// ].filter(Boolean)), [ blockQuery, blockTxsQuery, blockWithdrawalsQuery ]);
// const hasPagination = !isMobile && (
// (tab === 'txs' && blockTxsQuery.pagination.isVisible) ||
// (tab === 'withdrawals' && blockWithdrawalsQuery.pagination.isVisible)
// );
// let pagination;
// if (tab === 'txs') {
// pagination = blockTxsQuery.pagination;
// } else if (tab === 'withdrawals') {
// pagination = blockWithdrawalsQuery.pagination;
// }
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/zkevm_l2_txn_batches');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to tx batches list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `Tx batch #${ number }` }
backLink={ backLink }
/>
{ /* { batchQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationParams) }/> : null }
stickyEnabled={ hasPagination }
/>
) } */ }
<ZkEvmL2TxnBatchDetails query={ batchQuery }/>
</>
);
};
export default ZkEvmL2TxnBatch;
...@@ -6,7 +6,7 @@ import Hint from 'ui/shared/Hint'; ...@@ -6,7 +6,7 @@ import Hint from 'ui/shared/Hint';
interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> { interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode; title: React.ReactNode;
hint: string; hint?: string;
children: React.ReactNode; children: React.ReactNode;
note?: string; note?: string;
isLoading?: boolean; isLoading?: boolean;
...@@ -17,7 +17,7 @@ const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles ...@@ -17,7 +17,7 @@ const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles
<> <>
<GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="flex-start"> <Flex columnGap={ 2 } alignItems="flex-start">
<Hint label={ hint } isLoading={ isLoading }/> { hint && <Hint label={ hint } isLoading={ isLoading }/> }
<Skeleton isLoaded={ !isLoading }> <Skeleton isLoaded={ !isLoading }>
<Text fontWeight={{ base: 700, lg: 500 }}> <Text fontWeight={{ base: 700, lg: 500 }}>
{ title } { title }
......
...@@ -38,6 +38,7 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; ...@@ -38,6 +38,7 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
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';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData'; import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
...@@ -152,6 +153,15 @@ const TxDetails = () => { ...@@ -152,6 +153,15 @@ const TxDetails = () => {
</Tag> </Tag>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
{ data.zkevm_status && (
<DetailsInfoItem
title="ZkEVM status"
// hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
isLoading={ isPlaceholderData }
>
{ data.zkevm_status }
</DetailsInfoItem>
) }
{ data.revert_reason && ( { data.revert_reason && (
<DetailsInfoItem <DetailsInfoItem
title="Revert reason" title="Revert reason"
...@@ -182,6 +192,19 @@ const TxDetails = () => { ...@@ -182,6 +192,19 @@ const TxDetails = () => {
</> </>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
{ data.zkevm_batch_number && (
<DetailsInfoItem
title="Tx batch"
// hint="Block number containing the transaction"
isLoading={ isPlaceholderData }
>
<BlockEntityL2
isLoading={ isPlaceholderData }
number={ data.zkevm_batch_number }
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: data.zkevm_batch_number.toString() } }) }
/>
</DetailsInfoItem>
) }
{ data.timestamp && ( { data.timestamp && (
<DetailsInfoItem <DetailsInfoItem
title="Timestamp" title="Timestamp"
...@@ -286,6 +309,27 @@ const TxDetails = () => { ...@@ -286,6 +309,27 @@ const TxDetails = () => {
<DetailsInfoItemDivider/> <DetailsInfoItemDivider/>
{ data.zkevm_batch_number && (
<DetailsInfoItem
title="Sequence tx hash"
// hint="Value sent in the native token (and USD) if applicable"
isLoading={ isPlaceholderData }
>
{ data.zkevm_sequence_hash }
</DetailsInfoItem>
) }
{ data.zkevm_verify_hash && (
<DetailsInfoItem
title="Verify tx hash"
// hint="Value sent in the native token (and USD) if applicable"
isLoading={ isPlaceholderData }
>
{ data.zkevm_verify_hash }
</DetailsInfoItem>
) }
{ (data.zkevm_batch_number || data.zkevm_verify_hash) && <DetailsInfoItemDivider/> }
{ !config.UI.views.tx.hiddenFields?.value && ( { !config.UI.views.tx.hiddenFields?.value && (
<DetailsInfoItem <DetailsInfoItem
title="Value" title="Value"
......
import { Grid, GridItem, Text, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvml2TxnBatches';
import clockIcon from 'icons/clock.svg';
import type { ResourceError } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import PrevNext from 'ui/shared/PrevNext';
import TextSeparator from 'ui/shared/TextSeparator';
interface Props {
query: UseQueryResult<ZkEvmL2TxnBatch, ResourceError>;
}
const ZkEvmL2TxnBatchDetails = ({ query }: Props) => {
const router = useRouter();
const { data, isPlaceholderData, isError, error } = query;
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
}
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.number + increment);
router.push({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: nextId } }, undefined);
}, [ data, router ]);
if (isError) {
if (error?.status === 404) {
throw Error('Tx Batch not found', { cause: error as unknown as Error });
}
if (error?.status === 422) {
throw Error('Invalid tx batch number', { cause: error as unknown as Error });
}
return <DataFetchAlert/>;
}
if (!data) {
return null;
}
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: 3, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
overflow="hidden"
>
<DetailsInfoItem
title="Tx batch number"
// hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.number }
</Skeleton>
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous tx batch"
nextLabel="View next tx batch"
isPrevDisabled={ data.number === 0 }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem>
<DetailsInfoItem
title="Status"
// hint="Size of the block in bytes"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.status }
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Verify timestamp"
// hint="Date & time at which block was produced."
isLoading={ isPlaceholderData }
>
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
{ dayjs(data.timestamp).fromNow() }
</Skeleton>
<TextSeparator/>
<Skeleton isLoaded={ !isPlaceholderData } whiteSpace="normal">
{ dayjs(data.timestamp).format('llll') }
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Verify tx hash"
// hint="Date & time at which block was produced."
isLoading={ isPlaceholderData }
>
{ data.verify_tx_hash ? (
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.verify_tx_hash }
maxW="100%"
/>
) : <Text>pending</Text> }
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="The number of transactions in the batch"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ /* <LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'txs' } }) }> */ }
{ data.transactions.length } transaction{ data.transactions.length === 1 ? '' : 's' }
{ /* </LinkInternal> */ }
</Skeleton>
</DetailsInfoItem>
{ sectionGap }
<DetailsInfoItem
title="Global exit root"
// hint=''
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.global_exit_root }
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Acc input hash"
// hint=''
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.acc_input_hash }
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Sequence tx hash"
// hint=''
isLoading={ isPlaceholderData }
>
{ data.sequence_tx_hash ? (
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.sequence_tx_hash }
maxW="100%"
/>
) : <Text>pending</Text> }
{ /* Not sertain how to display pending state */ }
</DetailsInfoItem>
<DetailsInfoItem
title="State root"
// hint=''
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.state_root }
</Skeleton>
</DetailsInfoItem>
</Grid>
);
};
export default ZkEvmL2TxnBatchDetails;
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvml2TxnBatches'; import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvml2TxnBatches';
// import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
...@@ -34,8 +34,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => { ...@@ -34,8 +34,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
fontWeight={ 600 } fontWeight={ 600 }
// fix after implementing batch page href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: item.number.toString() } }) }
href="#"
/> />
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
......
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvml2TxnBatches'; import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvml2TxnBatches';
// import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
...@@ -31,6 +31,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => { ...@@ -31,6 +31,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
fontWeight={ 600 } fontWeight={ 600 }
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: item.number.toString() } }) }
/> />
</Td> </Td>
<Td> <Td>
......
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