Commit 38b59af5 authored by tom goriunov's avatar tom goriunov Committed by GitHub

zkSync batches (#1743)

* dev preset

* batches page

* batch details view

* batch txs

* adjustments in block view

* adjustments in tx view

* adjustments on home page

* fixes

* tests

* add pagination to batch txs

* ENV values for demo

* fix layout on batch details tab

* add more fields to batch details

* update screenshots

* revert demo ENVs
parent b1acf6ae
...@@ -343,6 +343,7 @@ ...@@ -343,6 +343,7 @@
"sepolia", "sepolia",
"polygon", "polygon",
"zkevm", "zkevm",
"zksync",
"gnosis", "gnosis",
"rootstock", "rootstock",
"stability", "stability",
......
# Set of ENVs for zkSync (dev only)
# https://zksync.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=ZkSync Era
NEXT_PUBLIC_NETWORK_SHORT_NAME=ZkSync Era
NEXT_PUBLIC_NETWORK_ID=324
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.era.zksync.io
# api configuration
NEXT_PUBLIC_API_HOST=zksync.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync-dark.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short-dark.svg
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgba(53, 103, 246, 1)'
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(255, 255, 255, 1)'
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://zksync.drpc.org?ref=559183','text':'Public RPC'}]
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/zksync.json
## views
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=false
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'l2scan','baseUrl':'https://zksync-era.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zksync.png
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x79c7802ccdf3be5a49c47cc751aad351b0027e8275f6f54878eda50ee559a648
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-zksync.safe.global
# rollup
NEXT_PUBLIC_ROLLUP_TYPE=zkSync
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com
...@@ -97,6 +97,7 @@ import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; ...@@ -97,6 +97,7 @@ import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
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 { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2'; import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2';
import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2';
import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { MarketplaceAppOverview } from 'types/client/marketplace';
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
...@@ -561,6 +562,9 @@ export const RESOURCES = { ...@@ -561,6 +562,9 @@ export const RESOURCES = {
homepage_zkevm_latest_batch: { homepage_zkevm_latest_batch: {
path: '/api/v2/main-page/zkevm/batches/latest-number', path: '/api/v2/main-page/zkevm/batches/latest-number',
}, },
homepage_zksync_latest_batch: {
path: '/api/v2/main-page/zksync/batches/latest-number',
},
// SEARCH // SEARCH
quick_search: { quick_search: {
...@@ -575,7 +579,7 @@ export const RESOURCES = { ...@@ -575,7 +579,7 @@ export const RESOURCES = {
path: '/api/v2/search/check-redirect', path: '/api/v2/search/check-redirect',
}, },
// L2 // optimistic L2
l2_deposits: { l2_deposits: {
path: '/api/v2/optimism/deposits', path: '/api/v2/optimism/deposits',
filterFields: [], filterFields: [],
...@@ -612,6 +616,7 @@ export const RESOURCES = { ...@@ -612,6 +616,7 @@ export const RESOURCES = {
path: '/api/v2/optimism/txn-batches/count', path: '/api/v2/optimism/txn-batches/count',
}, },
// zkEvm L2
zkevm_l2_txn_batches: { zkevm_l2_txn_batches: {
path: '/api/v2/zkevm/batches', path: '/api/v2/zkevm/batches',
filterFields: [], filterFields: [],
...@@ -625,12 +630,34 @@ export const RESOURCES = { ...@@ -625,12 +630,34 @@ export const RESOURCES = {
path: '/api/v2/zkevm/batches/:number', path: '/api/v2/zkevm/batches/:number',
pathParams: [ 'number' as const ], pathParams: [ 'number' as const ],
}, },
zkevm_l2_txn_batch_txs: { zkevm_l2_txn_batch_txs: {
path: '/api/v2/transactions/zkevm-batch/:number', path: '/api/v2/transactions/zkevm-batch/:number',
pathParams: [ 'number' as const ], pathParams: [ 'number' as const ],
filterFields: [], filterFields: [],
}, },
// zkSync L2
zksync_l2_txn_batches: {
path: '/api/v2/zksync/batches',
filterFields: [],
},
zksync_l2_txn_batches_count: {
path: '/api/v2/zksync/batches/count',
},
zksync_l2_txn_batch: {
path: '/api/v2/zksync/batches/:number',
pathParams: [ 'number' as const ],
},
zksync_l2_txn_batch_txs: {
path: '/api/v2/transactions/zksync-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
// SHIBARIUM L2 // SHIBARIUM L2
shibarium_deposits: { shibarium_deposits: {
path: '/api/v2/shibarium/deposits', path: '/api/v2/shibarium/deposits',
...@@ -770,6 +797,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -770,6 +797,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'shibarium_deposits' | 'shibarium_withdrawals' | 'shibarium_deposits' | 'shibarium_withdrawals' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history'; 'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history';
...@@ -801,6 +829,7 @@ Q extends 'homepage_deposits' ? Array<OptimisticL2DepositsItem> : ...@@ -801,6 +829,7 @@ Q extends 'homepage_deposits' ? Array<OptimisticL2DepositsItem> :
Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } : Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'homepage_zkevm_latest_batch' ? number : Q extends 'homepage_zkevm_latest_batch' ? number :
Q extends 'homepage_zksync_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 :
...@@ -892,6 +921,10 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : ...@@ -892,6 +921,10 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_withdrawals_count' ? number :
Q extends 'shibarium_deposits_count' ? number : Q extends 'shibarium_deposits_count' ? number :
Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse :
Q extends 'zksync_l2_txn_batches_count' ? number :
Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch :
Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs :
Q extends 'contract_security_audits' ? SmartContractSecurityAudits : Q extends 'contract_security_audits' ? SmartContractSecurityAudits :
Q extends 'addresses_lookup' ? EnsAddressLookupResponse : Q extends 'addresses_lookup' ? EnsAddressLookupResponse :
Q extends 'domain_info' ? EnsDomainDetailed : Q extends 'domain_info' ? EnsDomainDetailed :
......
...@@ -136,6 +136,26 @@ export default function useNavItems(): ReturnType { ...@@ -136,6 +136,26 @@ export default function useNavItems(): ReturnType {
ensLookup, ensLookup,
].filter(Boolean), ].filter(Boolean),
]; ];
} else if (rollupFeature.isEnabled && rollupFeature.type === 'zkSync') {
blockchainNavItems = [
[
txs,
userOps,
blocks,
{
text: 'Txn batches',
nextRoute: { pathname: '/batches' as const },
icon: 'txn_batches',
isActive: pathname === '/batches' || pathname === '/batches/[number]',
},
].filter(Boolean),
[
topAccounts,
validators,
verifiedContracts,
ensLookup,
].filter(Boolean),
];
} else { } else {
blockchainNavItems = [ blockchainNavItems = [
txs, txs,
......
import type { ZkSyncBatch } from 'types/api/zkSyncL2';
export const base: ZkSyncBatch = {
commit_transaction_hash: '0x7cd80c88977c2b310f79196b0b2136da18012be015ce80d0d9e9fe6cfad52b16',
commit_transaction_timestamp: '2022-03-19T09:37:38.726996Z',
end_block: 1245490,
execute_transaction_hash: '0x110b9a19afbabd5818a996ab2b493a9b23c888d73d95f1ab5272dbae503e103a',
execute_transaction_timestamp: '2022-03-19T10:29:05.358066Z',
l1_gas_price: '4173068062',
l1_tx_count: 0,
l2_fair_gas_price: '100000000',
l2_tx_count: 287,
number: 8051,
prove_transaction_hash: '0xb424162ba5afe17c710dceb5fc8d15d7d46a66223454dae8c74aa39f6802625b',
prove_transaction_timestamp: '2022-03-19T10:29:05.279179Z',
root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792',
start_block: 1245209,
status: 'Executed on L1',
timestamp: '2022-03-19T09:05:49.000000Z',
};
import type { ZkSyncBatchesItem, ZkSyncBatchesResponse } from 'types/api/zkSyncL2';
export const sealed: ZkSyncBatchesItem = {
commit_transaction_hash: null,
commit_transaction_timestamp: null,
execute_transaction_hash: null,
execute_transaction_timestamp: null,
number: 8055,
prove_transaction_hash: null,
prove_transaction_timestamp: null,
status: 'Sealed on L2',
timestamp: '2022-03-19T12:53:36.000000Z',
tx_count: 738,
};
export const sent: ZkSyncBatchesItem = {
commit_transaction_hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661',
commit_transaction_timestamp: '2022-03-19T13:09:07.357570Z',
execute_transaction_hash: null,
execute_transaction_timestamp: null,
number: 8054,
prove_transaction_hash: null,
prove_transaction_timestamp: null,
status: 'Sent to L1',
timestamp: '2022-03-19T11:36:45.000000Z',
tx_count: 766,
};
export const executed: ZkSyncBatchesItem = {
commit_transaction_hash: '0xa2628f477e1027ac1c60fa75c186b914647769ac1cb9c7e1cab50b13506a0035',
commit_transaction_timestamp: '2022-03-19T11:52:18.963659Z',
execute_transaction_hash: '0xb7bd6b2b17498c66d3f6e31ac3685133a81b7f728d4f6a6f42741daa257d0d68',
execute_transaction_timestamp: '2022-03-19T13:28:16.712656Z',
number: 8053,
prove_transaction_hash: '0x9d44f2b775bd771f8a53205755b3897929aa672d2cd419b3b988c16d41d4f21e',
prove_transaction_timestamp: '2022-03-19T13:28:16.603104Z',
status: 'Executed on L1',
timestamp: '2022-03-19T10:01:52.000000Z',
tx_count: 1071,
};
export const baseResponse: ZkSyncBatchesResponse = {
items: [
sealed,
sent,
executed,
],
next_page_params: null,
};
...@@ -108,8 +108,8 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => { ...@@ -108,8 +108,8 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const zkEvmRollup: GetServerSideProps<Props> = async(context) => { export const batch: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm')) { if (!(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync'))) {
return { return {
notFound: true, notFound: true,
}; };
......
...@@ -5,16 +5,32 @@ import React from 'react'; ...@@ -5,16 +5,32 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
const ZkEvmL2TxnBatch = dynamic(() => import('ui/pages/ZkEvmL2TxnBatch'), { ssr: false }); import config from 'configs/app';
const rollupFeature = config.features.rollup;
const Batch = dynamic(() => {
if (!rollupFeature.isEnabled) {
throw new Error('Rollup feature is not enabled.');
}
switch (rollupFeature.type) {
case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync':
return import('ui/pages/ZkSyncL2TxnBatch');
}
throw new Error('Txn batches feature is not enabled.');
}, { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/batches/[number]" query={ props }> <PageNextJs pathname="/batches/[number]" query={ props }>
<ZkEvmL2TxnBatch/> <Batch/>
</PageNextJs> </PageNextJs>
); );
}; };
export default Page; export default Page;
export { zkEvmRollup as getServerSideProps } from 'nextjs/getServerSideProps'; export { batch as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -15,6 +15,8 @@ const Batches = dynamic(() => { ...@@ -15,6 +15,8 @@ const Batches = dynamic(() => {
switch (rollupFeature.type) { switch (rollupFeature.type) {
case 'zkEvm': case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatches'); return import('ui/pages/ZkEvmL2TxnBatches');
case 'zkSync':
return import('ui/pages/ZkSyncL2TxnBatches');
case 'optimistic': case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatches'); return import('ui/pages/OptimisticL2TxnBatches');
} }
......
...@@ -41,6 +41,10 @@ export const featureEnvs = { ...@@ -41,6 +41,10 @@ export const featureEnvs = {
{ name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'zkEvm' }, { name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'zkEvm' },
{ name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' }, { name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' },
], ],
zkSyncRollup: [
{ name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'zkSync' },
{ name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' },
],
userOps: [ userOps: [
{ name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' }, { name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' },
], ],
......
import type { ZkSyncBatch, ZkSyncBatchesItem } from 'types/api/zkSyncL2';
import { TX_HASH } from './tx';
export const ZKSYNC_L2_TXN_BATCHES_ITEM: ZkSyncBatchesItem = {
commit_transaction_hash: TX_HASH,
commit_transaction_timestamp: '2022-03-17T19:33:04.519145Z',
execute_transaction_hash: TX_HASH,
execute_transaction_timestamp: '2022-03-17T20:49:48.856345Z',
number: 8002,
prove_transaction_hash: TX_HASH,
prove_transaction_timestamp: '2022-03-17T20:49:48.772442Z',
status: 'Executed on L1',
timestamp: '2022-03-17T17:00:11.000000Z',
tx_count: 1215,
};
export const ZKSYNC_L2_TXN_BATCH: ZkSyncBatch = {
...ZKSYNC_L2_TXN_BATCHES_ITEM,
start_block: 1245209,
end_block: 1245490,
l1_gas_price: '4173068062',
l1_tx_count: 0,
l2_fair_gas_price: '100000000',
l2_tx_count: 287,
root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792',
};
...@@ -2,6 +2,8 @@ import type { AddressParam } from 'types/api/addressParams'; ...@@ -2,6 +2,8 @@ import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward'; import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { ZkSyncBatchesItem } from './zkSyncL2';
export type BlockType = 'block' | 'reorg' | 'uncle'; export type BlockType = 'block' | 'reorg' | 'uncle';
export interface Block { export interface Block {
...@@ -42,6 +44,10 @@ export interface Block { ...@@ -42,6 +44,10 @@ export interface Block {
burnt_blob_fees?: string; burnt_blob_fees?: string;
excess_blob_gas?: string; excess_blob_gas?: string;
blob_tx_count?: number; blob_tx_count?: number;
// ZKSYNC FIELDS
zksync?: Omit<ZkSyncBatchesItem, 'number' | 'tx_count' | 'timestamp'> & {
'batch_number': number | null;
};
} }
export interface BlocksResponse { export interface BlocksResponse {
......
...@@ -7,6 +7,7 @@ import type { OptimisticL2WithdrawalStatus } from './optimisticL2'; ...@@ -7,6 +7,7 @@ import type { OptimisticL2WithdrawalStatus } from './optimisticL2';
import type { TokenInfo } from './token'; import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction'; import type { TxAction } from './txAction';
import type { ZkSyncBatchesItem } from './zkSyncL2';
export type TransactionRevertReason = { export type TransactionRevertReason = {
raw: string; raw: string;
...@@ -80,6 +81,10 @@ export type Transaction = { ...@@ -80,6 +81,10 @@ export type Transaction = {
zkevm_batch_number?: number; zkevm_batch_number?: number;
zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number]; zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number];
zkevm_sequence_hash?: string; zkevm_sequence_hash?: string;
// zkSync FIELDS
zksync?: Omit<ZkSyncBatchesItem, 'number' | 'tx_count' | 'timestamp'> & {
'batch_number': number | null;
};
// blob tx fields // blob tx fields
blob_versioned_hashes?: Array<string>; blob_versioned_hashes?: Array<string>;
blob_gas_used?: string; blob_gas_used?: string;
......
import type { Transaction } from './transaction';
export const ZKSYNC_L2_TX_BATCH_STATUSES = [
'Processed on L2' as const,
'Sealed on L2' as const,
'Sent to L1' as const,
'Validated on L1' as const,
'Executed on L1' as const,
];
export type ZkSyncBatchStatus = typeof ZKSYNC_L2_TX_BATCH_STATUSES[number];
export interface ZkSyncBatchesItem {
commit_transaction_hash: string | null;
commit_transaction_timestamp: string | null;
execute_transaction_hash: string | null;
execute_transaction_timestamp: string | null;
number: number;
prove_transaction_hash: string | null;
prove_transaction_timestamp: string | null;
status: ZkSyncBatchStatus;
timestamp: string;
tx_count: number;
}
export type ZkSyncBatchesResponse = {
items: Array<ZkSyncBatchesItem>;
next_page_params: {
number: number;
items_count: number;
} | null;
}
export interface ZkSyncBatch extends Omit<ZkSyncBatchesItem, 'tx_count'> {
start_block: number;
end_block: number;
l1_gas_price: string;
l1_tx_count: number;
l2_fair_gas_price: string;
l2_tx_count: number;
root_hash: string;
}
export type ZkSyncBatchTxs = {
items: Array<Transaction>;
next_page_params: {
batch_number: string;
block_number: number;
index: number;
items_count: number;
} | null;
}
...@@ -4,6 +4,7 @@ export const ROLLUP_TYPES = [ ...@@ -4,6 +4,7 @@ export const ROLLUP_TYPES = [
'optimistic', 'optimistic',
'shibarium', 'shibarium',
'zkEvm', 'zkEvm',
'zkSync',
] as const; ] as const;
export type RollupType = ArrayElement<typeof ROLLUP_TYPES>; export type RollupType = ArrayElement<typeof ROLLUP_TYPES>;
...@@ -5,6 +5,8 @@ import { useRouter } from 'next/router'; ...@@ -5,6 +5,8 @@ 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 { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
...@@ -19,6 +21,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; ...@@ -19,6 +21,7 @@ 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';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -27,6 +30,8 @@ import PrevNext from 'ui/shared/PrevNext'; ...@@ -27,6 +30,8 @@ import PrevNext from 'ui/shared/PrevNext';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; 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 VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
import BlockDetailsBlobInfo from './details/BlockDetailsBlobInfo'; import BlockDetailsBlobInfo from './details/BlockDetailsBlobInfo';
import type { BlockQuery } from './useBlockQuery'; import type { BlockQuery } from './useBlockQuery';
...@@ -214,6 +219,31 @@ const BlockDetails = ({ query }: Props) => { ...@@ -214,6 +219,31 @@ const BlockDetails = ({ query }: Props) => {
</Skeleton> </Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync && (
<>
<DetailsInfoItem
title="Batch"
hint="Batch number"
isLoading={ isPlaceholderData }
>
{ data.zksync.batch_number ? (
<BatchEntityL2
isLoading={ isPlaceholderData }
number={ data.zksync.batch_number }
/>
) : <Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
</DetailsInfoItem>
<DetailsInfoItem
title="Status"
hint="Status is the short interpretation of the batch lifecycle"
isLoading={ isPlaceholderData }
>
<VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES } currentStep={ data.zksync.status } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
</>
) }
{ !config.UI.views.block.hiddenFields?.miner && ( { !config.UI.views.block.hiddenFields?.miner && (
<DetailsInfoItem <DetailsInfoItem
title={ verificationTitle } title={ verificationTitle }
...@@ -388,6 +418,9 @@ const BlockDetails = ({ query }: Props) => { ...@@ -388,6 +418,9 @@ const BlockDetails = ({ query }: Props) => {
<> <>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/> <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync &&
<ZkSyncL2TxnBatchHashesInfo data={ data.zksync } isLoading={ isPlaceholderData }/> }
{ !isPlaceholderData && <BlockDetailsBlobInfo data={ data }/> } { !isPlaceholderData && <BlockDetailsBlobInfo data={ data }/> }
{ data.bitcoin_merged_mining_header && ( { data.bitcoin_merged_mining_header && (
......
...@@ -33,10 +33,21 @@ const Stats = () => { ...@@ -33,10 +33,21 @@ const Stats = () => {
}, },
}); });
if (isError || zkEvmLatestBatchQuery.isError) { const zkSyncLatestBatchQuery = useApiQuery('homepage_zksync_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkSync',
},
});
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError) {
return null; return null;
} }
const isLoading = isPlaceholderData ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData);
let content; let content;
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } }; const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
...@@ -52,7 +63,7 @@ const Stats = () => { ...@@ -52,7 +63,7 @@ const Stats = () => {
const gasInfoTooltip = hasGasTracker && data.gas_prices ? ( const gasInfoTooltip = hasGasTracker && data.gas_prices ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }> <GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
<IconSvg <IconSvg
isLoading={ isPlaceholderData } isLoading={ isLoading }
name="info" name="info"
boxSize={ 5 } boxSize={ 5 }
display="block" display="block"
...@@ -67,21 +78,31 @@ const Stats = () => { ...@@ -67,21 +78,31 @@ const Stats = () => {
content = ( content = (
<> <>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? ( { rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && (
<StatsItem <StatsItem
icon="txn_batches" icon="txn_batches"
title="Latest batch" title="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() } value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) } url={ route({ pathname: '/batches' }) }
isLoading={ zkEvmLatestBatchQuery.isPlaceholderData } isLoading={ isLoading }
/> />
) : ( ) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && (
<StatsItem
icon="txn_batches"
title="Latest batch"
value={ (zkSyncLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) }
isLoading={ isLoading }
/>
) }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && (
<StatsItem <StatsItem
icon="block" icon="block"
title="Total blocks" title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() } value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) } url={ route({ pathname: '/blocks' }) }
isLoading={ isPlaceholderData } isLoading={ isLoading }
/> />
) } ) }
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
...@@ -89,7 +110,7 @@ const Stats = () => { ...@@ -89,7 +110,7 @@ const Stats = () => {
icon="clock-light" icon="clock-light"
title="Average block time" title="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) }s` } value={ `${ (data.average_block_time / 1000).toFixed(1) }s` }
isLoading={ isPlaceholderData } isLoading={ isLoading }
/> />
) } ) }
<StatsItem <StatsItem
...@@ -97,14 +118,14 @@ const Stats = () => { ...@@ -97,14 +118,14 @@ const Stats = () => {
title="Total transactions" title="Total transactions"
value={ Number(data.total_transactions).toLocaleString() } value={ Number(data.total_transactions).toLocaleString() }
url={ route({ pathname: '/txs' }) } url={ route({ pathname: '/txs' }) }
isLoading={ isPlaceholderData } isLoading={ isLoading }
/> />
<StatsItem <StatsItem
icon="wallet" icon="wallet"
title="Wallet addresses" title="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() } value={ Number(data.total_addresses).toLocaleString() }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isPlaceholderData } isLoading={ isLoading }
/> />
{ hasGasTracker && data.gas_prices && ( { hasGasTracker && data.gas_prices && (
<StatsItem <StatsItem
...@@ -113,7 +134,7 @@ const Stats = () => { ...@@ -113,7 +134,7 @@ const Stats = () => {
value={ <GasPrice data={ data.gas_prices.average }/> } value={ <GasPrice data={ data.gas_prices.average }/> }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
tooltip={ gasInfoTooltip } tooltip={ gasInfoTooltip }
isLoading={ isPlaceholderData } isLoading={ isLoading }
/> />
) } ) }
{ data.rootstock_locked_btc && ( { data.rootstock_locked_btc && (
...@@ -122,7 +143,7 @@ const Stats = () => { ...@@ -122,7 +143,7 @@ const Stats = () => {
title="BTC Locked in 2WP" title="BTC Locked in 2WP"
value={ `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC` } value={ `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC` }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isPlaceholderData } isLoading={ isLoading }
/> />
) } ) }
</> </>
......
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import * as zkSyncTxnBatchMock from 'mocks/zkSync/zkSyncTxnBatch';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import ZkSyncL2TxnBatch from './ZkSyncL2TxnBatch';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkSyncRollup) as any,
});
const hooksConfig = {
router: {
query: { number: String(zkSyncTxnBatchMock.base.number) },
},
};
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(BATCH_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(zkSyncTxnBatchMock.base),
}));
});
const BATCH_API_URL = buildApiUrl('zksync_l2_txn_batch', { number: String(zkSyncTxnBatchMock.base.number) });
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<ZkSyncL2TxnBatch/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<ZkSyncL2TxnBatch/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { ZKSYNC_L2_TXN_BATCH } from 'stubs/zkSyncL2';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
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 ZkSyncL2TxnBatchDetails from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const ZkSyncL2TxnBatch = () => {
const router = useRouter();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
const tab = getQueryParamString(router.query.tab);
const isMobile = useIsMobile();
const batchQuery = useApiQuery('zksync_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: ZKSYNC_L2_TXN_BATCH,
},
});
const batchTxsQuery = useQueryWithPages({
resourceName: 'zksync_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'txs'),
placeholderData: generateListStub<'zksync_l2_txn_batch_txs'>(TX, 50, { next_page_params: {
batch_number: '8122',
block_number: 1338932,
index: 0,
items_count: 50,
} }),
},
});
throwOnAbsentParamError(number);
throwOnResourceLoadError(batchQuery);
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkSyncL2TxnBatchDetails query={ batchQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false }/> },
].filter(Boolean)), [ batchQuery, batchTxsQuery ]);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to tx batches list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
const hasPagination = !isMobile && batchTxsQuery.pagination.isVisible && tab === 'txs';
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 { ...(batchTxsQuery.pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
</>
);
};
export default ZkSyncL2TxnBatch;
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as zkSyncTxnBatchesMock from 'mocks/zkSync/zkSyncTxnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import ZkSyncL2TxnBatches from './ZkSyncL2TxnBatches';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkSyncRollup) as any,
});
const BATCHES_API_URL = buildApiUrl('zksync_l2_txn_batches');
const BATCHES_COUNTERS_API_URL = buildApiUrl('zksync_l2_txn_batches_count');
test('base view +@mobile', async({ mount, page }) => {
test.slow();
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(BATCHES_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(zkSyncTxnBatchesMock.baseResponse),
}));
await page.route(BATCHES_COUNTERS_API_URL, (route) => route.fulfill({
status: 200,
body: '9927',
}));
const component = await mount(
<TestApp>
<ZkSyncL2TxnBatches/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { generateListStub } from 'stubs/utils';
import { ZKSYNC_L2_TXN_BATCHES_ITEM } from 'stubs/zkSyncL2';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import ZkSyncTxnBatchesListItem from 'ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesListItem';
import ZkSyncTxnBatchesTable from 'ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTable';
const ZkSyncL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'zksync_l2_txn_batches',
options: {
placeholderData: generateListStub<'zksync_l2_txn_batches'>(
ZKSYNC_L2_TXN_BATCHES_ITEM,
50,
{
next_page_params: {
items_count: 50,
number: 9045200,
},
},
),
},
});
const countersQuery = useApiQuery('zksync_l2_txn_batches_count', {
queryOptions: {
placeholderData: 5231746,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ZkSyncTxnBatchesListItem
key={ item.number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }><ZkSyncTxnBatchesTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/></Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isError || isError || !data?.items.length) {
return null;
}
return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text>
(total of { countersQuery.data?.toLocaleString() } batches)
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title="Tx batches" withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no tx batches."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ZkSyncL2TxnBatches;
import React from 'react';
import type { ZkSyncBatchStatus } from 'types/api/zkSyncL2';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
export interface Props {
status: ZkSyncBatchStatus;
isLoading?: boolean;
}
const ZkSyncL2TxnBatchStatus = ({ status, isLoading }: Props) => {
let type: StatusTagType;
switch (status) {
case 'Executed on L1':
type = 'ok';
break;
default:
type = 'pending';
break;
}
return <StatusTag type={ type } text={ status } isLoading={ isLoading }/>;
};
export default ZkSyncL2TxnBatchStatus;
...@@ -17,6 +17,7 @@ import { scroller, Element } from 'react-scroll'; ...@@ -17,6 +17,7 @@ import { scroller, Element } from 'react-scroll';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction'; import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction';
import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -54,6 +55,7 @@ import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus'; ...@@ -54,6 +55,7 @@ import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers'; import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
...@@ -143,7 +145,11 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -143,7 +145,11 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title={ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? 'L2 status and method' : 'Status and method' } title={
rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync') ?
'L2 status and method' :
'Status and method'
}
hint="Current transaction state: Success, Failed (Error), or Pending (In Process)" hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
isLoading={ isLoading } isLoading={ isLoading }
> >
...@@ -192,6 +198,15 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -192,6 +198,15 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<TxRevertReason { ...data.revert_reason }/> <TxRevertReason { ...data.revert_reason }/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.zksync && (
<DetailsInfoItem
title="L1 status"
hint="Status is the short interpretation of the batch lifecycle"
isLoading={ isLoading }
>
<VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES } currentStep={ data.zksync.status } isLoading={ isLoading }/>
</DetailsInfoItem>
) }
<DetailsInfoItem <DetailsInfoItem
title="Block" title="Block"
hint="Block number containing the transaction" hint="Block number containing the transaction"
...@@ -226,6 +241,20 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -226,6 +241,20 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
/> />
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.zksync && (
<DetailsInfoItem
title="Batch"
hint="Batch number"
isLoading={ isLoading }
>
{ data.zksync.batch_number ? (
<BatchEntityL2
isLoading={ isLoading }
number={ data.zksync.batch_number }
/>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
</DetailsInfoItem>
) }
{ data.timestamp && ( { data.timestamp && (
<DetailsInfoItem <DetailsInfoItem
title="Timestamp" title="Timestamp"
...@@ -564,6 +593,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -564,6 +593,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<LogDecodedInputData data={ data.decoded_input }/> <LogDecodedInputData data={ data.decoded_input }/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.zksync && <ZkSyncL2TxnBatchHashesInfo data={ data.zksync } isLoading={ isLoading }/> }
</> </>
) } ) }
</Grid> </Grid>
......
import { Grid, GridItem, Link, Skeleton, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import { ZKSYNC_L2_TX_BATCH_STATUSES, type ZkSyncBatch } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import { currencyUnits } from 'lib/units';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import LinkInternal from 'ui/shared/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import TruncatedValue from 'ui/shared/TruncatedValue';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
import ZkSyncL2TxnBatchHashesInfo from './ZkSyncL2TxnBatchHashesInfo';
interface Props {
query: UseQueryResult<ZkSyncBatch, ResourceError>;
}
const ZkSyncL2TxnBatchDetails = ({ query }: Props) => {
const router = useRouter();
const [ isExpanded, setIsExpanded ] = React.useState(false);
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: '/batches/[number]', query: { number: nextId } }, undefined);
}, [ data, router ]);
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
scroller.scrollTo('ZkSyncL2TxnBatchDetails__cutLink', {
duration: 500,
smooth: true,
});
}, []);
if (isError) {
if (isCustomAppError(error)) {
throwOnResourceLoadError({ isError, error });
}
return <DataFetchAlert/>;
}
if (!data) {
return null;
}
const txNum = data.l2_tx_count + data.l1_tx_count;
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="Batch number indicates the length of batches produced by grouping L2 blocks to be proven on Ethereum."
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="Status is the short interpretation of the batch lifecycle"
isLoading={ isPlaceholderData }
>
<VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES.slice(1) } currentStep={ data.status } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Timestamp"
hint="Date and time at which batch is produced"
isLoading={ isPlaceholderData }
>
{ data.timestamp ? <DetailsTimestamp timestamp={ data.timestamp } isLoading={ isPlaceholderData }/> : 'Undefined' }
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="Number of transactions inside the batch."
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ txNum } transaction{ txNum === 1 ? '' : 's' }
</LinkInternal>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItemDivider/>
<ZkSyncL2TxnBatchHashesInfo isLoading={ isPlaceholderData } data={ data }/>
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="ZkSyncL2TxnBatchDetails__cutLink">
<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 && (
<>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
<DetailsInfoItem
title="Root hash"
hint="L1 batch root is a hash that summarizes batch data and submitted to the L1"
flexWrap="nowrap"
alignSelf="flex-start"
>
<TruncatedValue value={ data.root_hash }/>
<CopyToClipboard text={ data.root_hash }/>
</DetailsInfoItem>
<DetailsInfoItem
title="L1 gas price"
hint="Gas price for the batch settlement transaction on L1"
>
<Text mr={ 1 }>{ BigNumber(data.l1_gas_price).dividedBy(WEI).toFixed() } { currencyUnits.ether }</Text>
<Text variant="secondary">({ BigNumber(data.l1_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="L2 fair gas price"
hint={ 'The gas price below which the "baseFee" of the batch should not fall' }
>
<Text mr={ 1 }>{ BigNumber(data.l2_fair_gas_price).dividedBy(WEI).toFixed() } { currencyUnits.ether }</Text>
<Text variant="secondary">({ BigNumber(data.l2_fair_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })</Text>
</DetailsInfoItem>
</>
) }
</Grid>
);
};
export default ZkSyncL2TxnBatchDetails;
import { Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ZkSyncBatch } from 'types/api/zkSyncL2';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
interface Props {
isLoading: boolean;
data: Pick<
ZkSyncBatch,
'commit_transaction_hash' |
'commit_transaction_timestamp' |
'prove_transaction_hash' |
'prove_transaction_timestamp' |
'execute_transaction_hash' |
'execute_transaction_timestamp'
>;
}
const ZkSyncL2TxnBatchHashesInfo = ({ isLoading, data }: Props) => {
return (
<>
<DetailsInfoItem
title="Commit tx hash"
hint="Hash of L1 tx on which the batch was committed"
isLoading={ isLoading }
flexDir="column"
alignItems="flex-start"
>
{ data.commit_transaction_hash ? (
<>
<TxEntityL1
isLoading={ isLoading }
hash={ data.commit_transaction_hash }
maxW="100%"
noCopy={ false }
/>
{ data.commit_transaction_timestamp && (
<Flex alignItems="center" flexWrap="wrap" rowGap={ 3 }>
<DetailsTimestamp timestamp={ data.commit_transaction_timestamp } isLoading={ isLoading }/>
</Flex>
) }
</>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
</DetailsInfoItem>
<DetailsInfoItem
title="Prove tx hash"
hint="Hash of L1 tx on which the batch was proven"
isLoading={ isLoading }
flexDir="column"
alignItems="flex-start"
>
{ data.prove_transaction_hash ? (
<>
<TxEntityL1
isLoading={ isLoading }
hash={ data.prove_transaction_hash }
maxW="100%"
noCopy={ false }
/>
{ data.prove_transaction_timestamp && (
<Flex alignItems="center" flexWrap="wrap" rowGap={ 3 }>
<DetailsTimestamp timestamp={ data.prove_transaction_timestamp } isLoading={ isLoading }/>
</Flex>
) }
</>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
</DetailsInfoItem>
<DetailsInfoItem
title="Execute tx hash"
hint="Hash of L1 tx on which the batch was executed and finalized"
isLoading={ isLoading }
flexDir="column"
alignItems="flex-start"
>
{ data.execute_transaction_hash ? (
<>
<TxEntityL1
isLoading={ isLoading }
hash={ data.execute_transaction_hash }
maxW="100%"
noCopy={ false }
/>
{ data.execute_transaction_timestamp && (
<Flex alignItems="center" flexWrap="wrap" rowGap={ 3 }>
<DetailsTimestamp timestamp={ data.execute_transaction_timestamp } isLoading={ isLoading }/>
</Flex>
) }
</>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
</DetailsInfoItem>
</>
);
};
export default React.memo(ZkSyncL2TxnBatchHashesInfo);
import { Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import type { ZkSyncBatchesItem } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ZkSyncL2TxnBatchStatus from 'ui/shared/statusTag/ZkSyncL2TxnBatchStatus';
const rollupFeature = config.features.rollup;
type Props = { item: ZkSyncBatchesItem; isLoading?: boolean };
const ZkSyncTxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined';
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkSync') {
return null;
}
return (
<ListItemMobileGrid.Container gridTemplateColumns="110px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ZkSyncL2TxnBatchStatus status={ item.status } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
fontWeight={ 600 }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Commit tx</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.commit_transaction_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.commit_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : <Text>Pending</Text> }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Prove tx</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.prove_transaction_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.prove_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : <Text>Pending</Text> }
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ZkSyncTxnBatchesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ZkSyncBatchesItem } from 'types/api/zkSyncL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import ZkSyncTxnBatchesTableItem from './ZkSyncTxnBatchesTableItem';
type Props = {
items: Array<ZkSyncBatchesItem>;
top: number;
isLoading?: boolean;
}
const ZkSyncTxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="1000px">
<Thead top={ top }>
<Tr>
<Th width="40%">Batch #</Th>
<Th width="60%">Status</Th>
<Th width="150px">Age</Th>
<Th width="150px">Txn count</Th>
<Th width="210px">Commit tx</Th>
<Th width="210px">Prove tx</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ZkSyncTxnBatchesTableItem
key={ item.number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default ZkSyncTxnBatchesTable;
import { Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ZkSyncBatchesItem } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ZkSyncL2TxnBatchStatus from 'ui/shared/statusTag/ZkSyncL2TxnBatchStatus';
const rollupFeature = config.features.rollup;
type Props = { item: ZkSyncBatchesItem; isLoading?: boolean };
const ZkSyncTxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined';
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkSync') {
return null;
}
return (
<Tr>
<Td verticalAlign="middle">
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
<ZkSyncL2TxnBatchStatus status={ item.status } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
</Skeleton>
</LinkInternal>
</Td>
<Td verticalAlign="middle">
{ item.commit_transaction_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.commit_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
) : <Text>Pending</Text> }
</Td>
<Td verticalAlign="middle">
{ item.prove_transaction_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.prove_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
) : <Text>Pending</Text> }
</Td>
</Tr>
);
};
export default ZkSyncTxnBatchesTableItem;
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