Commit ab04ff68 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #1261 from blockscout/zkevm-batches

Zkevm batches
parents 5843bd04 7005a940
......@@ -11,7 +11,7 @@ export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
export { default as optimisticRollup } from './optimisticRollup';
export { default as safe } from './safe';
export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml';
......@@ -19,3 +19,4 @@ export { default as stats } from './stats';
export { default as suave } from './suave';
export { default as web3Wallet } from './web3Wallet';
export { default as verifiedTokens } from './verifiedTokens';
export { default as zkEvmRollup } from './zkEvmRollup';
......@@ -6,10 +6,10 @@ const title = 'Rollup (L2) chain';
const config: Feature<{ L1BaseUrl: string; withdrawalUrl: string }> = (() => {
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_L1_BASE_URL');
const withdrawalUrl = getEnvValue('NEXT_PUBLIC_L2_WITHDRAWAL_URL');
const withdrawalUrl = getEnvValue('NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL');
if (
getEnvValue('NEXT_PUBLIC_IS_L2_NETWORK') === 'true' &&
getEnvValue('NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK') === 'true' &&
L1BaseUrl &&
withdrawalUrl
) {
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const title = 'ZkEVM rollup (L2) chain';
const config: Feature<{ L1BaseUrl: string; withdrawalUrl?: string }> = (() => {
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_L1_BASE_URL');
const isZkEvm = getEnvValue('NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK') === 'true';
if (isZkEvm && L1BaseUrl) {
return Object.freeze({
title,
isEnabled: true,
L1BaseUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -44,7 +44,8 @@ NEXT_PUBLIC_APP_INSTANCE=jest
NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
NEXT_PUBLIC_IS_L2_NETWORK=false
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=false
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=false
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
......
......@@ -47,6 +47,6 @@ NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_L2_NETWORK=true
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
......@@ -40,7 +40,8 @@ NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
NEXT_PUBLIC_IS_L2_NETWORK=false
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=false
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=false
NEXT_PUBLIC_AD_BANNER_PROVIDER=slise
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
......
# Set of ENVs for zkevm (dev only)
# https://eth.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=zkEVM
NEXT_PUBLIC_NETWORK_SHORT_NAME=zkEVM
NEXT_PUBLIC_NETWORK_ID=1
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://eth.llamarpc.com
# api configuration
NEXT_PUBLIC_API_HOST=65.109.173.70
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/dev/configs/featured-networks/eth.json
## footer
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.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
# rollup
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=http://65.109.173.70:81
......@@ -106,20 +106,23 @@ const beaconChainSchema = yup
const rollupSchema = yup
.object()
.shape({
NEXT_PUBLIC_IS_L2_NETWORK: yup.boolean(),
NEXT_PUBLIC_L1_BASE_URL: yup
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: yup.boolean(),
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: yup
.string()
.when('NEXT_PUBLIC_IS_L2_NETWORK', {
is: (value: boolean) => value,
.when('NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', {
is: (value: string) => value,
then: (schema) => schema.test(urlTest).required(),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_L1_BASE_URL cannot not be used if NEXT_PUBLIC_IS_L2_NETWORK is not set to "true"'),
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL cannot not be used if NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK is not set to "true"'),
}),
NEXT_PUBLIC_L2_WITHDRAWAL_URL: yup
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK: yup.boolean(),
NEXT_PUBLIC_L1_BASE_URL: yup
.string()
.when('NEXT_PUBLIC_IS_L2_NETWORK', {
is: (value: string) => value,
.when([ 'NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK' ], {
is: (isOptimistic?: boolean, isZk?: boolean) => isOptimistic || isZk,
then: (schema) => schema.test(urlTest).required(),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_L2_WITHDRAWAL_URL cannot not be used if NEXT_PUBLIC_IS_L2_NETWORK is not set to "true"'),
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_L1_BASE_URL cannot not be used if NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK or NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK is not set to "true"'),
}),
});
......
NEXT_PUBLIC_IS_L2_NETWORK=true
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=https://example.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL=https://example.com
\ No newline at end of file
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://example.com
\ No newline at end of file
......@@ -190,9 +190,9 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer-test.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_L2_NETWORK: "true"
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: "true"
NEXT_PUBLIC_L1_BASE_URL: https://eth-goerli.blockscout.com/
NEXT_PUBLIC_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
envFromSecret:
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
......
......@@ -104,8 +104,6 @@ frontend:
_default: https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: ''
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND:
_default: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
NEXT_PUBLIC_NETWORK_RPC_URL:
......@@ -124,12 +122,12 @@ frontend:
_default: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST:
_default: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_L2_NETWORK:
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK:
_default: "true"
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_L1_BASE_URL:
_default: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_SENTRY_DSN:
......
......@@ -33,7 +33,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Banner ads](ENVS.md#banner-ads)
- [Text ads](ENVS.md#text-ads)
- [Beacon chain](ENVS.md#beacon-chain)
- [Rollup (L2) chain](ENVS.md#rollup-l2-chain)
- [Optimistic rollup (L2) chain](ENVS.md#optimistic-rollup-l2-chain)
- [ZkEvm rollup (L2) chain](NVS.md#zkevm-rollup-l2-chain)
- [Export data to CSV file](ENVS.md#export-data-to-csv-file)
- [Google analytics](ENVS.md#google-analytics)
- [Mixpanel analytics](ENVS.md#mixpanel-analytics)
......@@ -337,13 +338,22 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp;
### Rollup (L2) chain
### Optimistic rollup (L2) chain
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_L2_NETWORK | `boolean` | Set to true for L2 solutions | Required | - | `true` |
| NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK | `boolean` | Set to true for optimistic L2 solutions | Required | - | `true` |
| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` |
| NEXT_PUBLIC_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` |
&nbsp;
### ZkEvm rollup (L2) chain
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` |
&nbsp;
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="currentColor" d="M7.79 11.839a1 1 0 0 0 1.42 0l5.285-5.33a.71.71 0 1 1 1.009 1L9.21 13.854a1 1 0 0 1-1.42 0l-3.294-3.322a.71.71 0 1 1 1.008-1L7.79 11.84Z"/>
<rect width="18.4" height="18.4" x=".8" y=".8" stroke="currentColor" stroke-width="1.6" rx="3.2"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<rect width="18.4" height="18.4" x=".8" y=".8" stroke="currentColor" stroke-width="1.6" rx="3.2"/>
</svg>
......@@ -17,6 +17,7 @@ const PAGE_PROPS = {
id: '',
height_or_hash: '',
hash: '',
number: '',
q: '',
};
......
......@@ -59,6 +59,7 @@ import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2TxnBatches';
import type { ArrayElement } from 'types/utils';
import config from 'configs/app';
......@@ -426,12 +427,18 @@ export const RESOURCES = {
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
homepage_zkevm_l2_batches: {
path: '/api/v2/main-page/zkevm/batches/confirmed',
},
homepage_txs_watchlist: {
path: '/api/v2/main-page/transactions/watchlist',
},
homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status',
},
homepage_zkevm_latest_batch: {
path: '/api/v2/main-page/zkevm/batches/latest-number',
},
// SEARCH
quick_search: {
......@@ -483,6 +490,25 @@ export const RESOURCES = {
path: '/api/v2/optimism/txn-batches/count',
},
zkevm_l2_txn_batches: {
path: '/api/v2/zkevm/batches',
filterFields: [],
},
zkevm_l2_txn_batches_count: {
path: '/api/v2/zkevm/batches/count',
},
zkevm_l2_txn_batch: {
path: '/api/v2/zkevm/batches/:number',
pathParams: [ 'number' as const ],
},
zkevm_l2_txn_batch_txs: {
path: '/api/v2/transactions/zkevm-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
// CONFIGS
config_backend_version: {
path: '/api/v2/config/backend-version',
......@@ -552,6 +578,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -575,7 +602,9 @@ Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'homepage_zkevm_latest_batch' ? number :
Q extends 'stats_counters' ? Counters :
Q extends 'stats_lines' ? StatsCharts :
Q extends 'stats_line' ? StatsChart :
......@@ -640,6 +669,10 @@ Q extends 'l2_output_roots_count' ? number :
Q extends 'l2_withdrawals_count' ? number :
Q extends 'l2_deposits_count' ? number :
Q extends 'l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse :
Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'config_backend_version' ? BackendVersionConfig :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -13,6 +13,7 @@ const AppContext = createContext<PageProps>({
id: '',
height_or_hash: '',
hash: '',
number: '',
q: '',
});
......
......@@ -71,7 +71,20 @@ export default function useNavItems(): ReturnType {
// eslint-disable-next-line max-len
{ text: 'Verified contracts', nextRoute: { pathname: '/verified-contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified-contracts' };
if (config.features.rollup.isEnabled) {
if (config.features.zkEvmRollup.isEnabled) {
blockchainNavItems = [
[
txs,
blocks,
// eslint-disable-next-line max-len
{ text: 'Txn batches', nextRoute: { pathname: '/zkevm-l2-txn-batches' as const }, icon: txnBatchIcon, isActive: pathname === '/zkevm-l2-txn-batches' || pathname === '/zkevm-l2-txn-batch/[number]' },
],
[
topAccounts,
verifiedContracts,
].filter(Boolean),
];
} else if (config.features.optimisticRollup.isEnabled) {
blockchainNavItems = [
[
txs,
......
......@@ -36,6 +36,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/l2-output-roots': 'Root page',
'/l2-txn-batches': 'Root page',
'/l2-withdrawals': 'Root page',
'/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page',
'/404': 'Regular page',
// service routes, added only to make typescript happy
......
......@@ -39,6 +39,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-output-roots': DEFAULT_TEMPLATE,
'/l2-txn-batches': DEFAULT_TEMPLATE,
'/l2-withdrawals': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy
......
......@@ -34,6 +34,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-output-roots': 'output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/404': 'error - page not found',
// service routes, added only to make typescript happy
......
......@@ -34,6 +34,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/l2-output-roots': 'Output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-withdrawals': 'Withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
'/404': '404',
// service routes, added only to make typescript happy
......
......@@ -6,6 +6,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2TxnBatches';
export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
......@@ -30,6 +31,7 @@ SocketMessage.SmartContractWasVerified |
SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply |
SocketMessage.ContractVerification |
SocketMessage.NewZkEvmL2Batch |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -64,5 +66,6 @@ export namespace SocketMessage {
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2TxnBatches';
export const txnBatchData: ZkEvmL2TxnBatch = {
acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3',
global_exit_root: '0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5',
number: 5,
sequence_tx_hash: '0x7ae010e9758441b302db10282807358af460f38c49c618d26a897592f64977f7',
state_root: '0x183b4a38a4a6027947ceb93b323cc94c548c8c05cf605d73ca88351d77cae1a3',
status: 'Finalized',
timestamp: '2023-10-20T10:08:18.000000Z',
transactions: [
'0xb5d432c270057c223b973f3b5f00dbad32823d9ef26f3e8d97c819c7c573453a',
],
verify_tx_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f',
};
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: 5218591,
tx_count: 9,
},
],
next_page_params: {
number: 5902834,
items_count: 50,
},
};
......@@ -8,6 +8,7 @@ export type Props = {
id: string;
height_or_hash: string;
hash: string;
number: string;
q: string;
}
......@@ -19,6 +20,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
id: query.id?.toString() || '',
hash: query.hash?.toString() || '',
height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '',
q: query.q?.toString() || '',
},
};
......@@ -55,7 +57,17 @@ export const beaconChain: GetServerSideProps<Props> = async(context) => {
};
export const L2: GetServerSideProps<Props> = async(context) => {
if (!config.features.rollup.isEnabled) {
if (!config.features.optimisticRollup.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const zkEvmL2: GetServerSideProps<Props> = async(context) => {
if (!config.features.zkEvmRollup.isEnabled) {
return {
notFound: true,
};
......
......@@ -46,7 +46,9 @@ declare module "nextjs-routes" {
| StaticRoute<"/txs">
| StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">;
| StaticRoute<"/withdrawals">
| DynamicRoute<"/zkevm-l2-txn-batch/[number]", { "number": string }>
| StaticRoute<"/zkevm-l2-txn-batches">;
interface StaticRoute<Pathname> {
pathname: 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 { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const ZkEvmL2TxnBatches = dynamic(() => import('ui/pages/ZkEvmL2TxnBatches'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/zkevm-l2-txn-batches">
<ZkEvmL2TxnBatches/>
</PageNextJs>
);
};
export default Page;
export { zkEvmL2 as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -27,6 +27,7 @@ const defaultAppContext = {
id: '',
height_or_hash: '',
hash: '',
number: '',
q: '',
},
};
......
......@@ -14,10 +14,10 @@ export const featureEnvs = {
beaconChain: [
{ name: 'NEXT_PUBLIC_HAS_BEACON_CHAIN', value: 'true' },
],
rollup: [
{ name: 'NEXT_PUBLIC_IS_L2_NETWORK', value: 'true' },
optimisticRollup: [
{ name: 'NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
{ name: 'NEXT_PUBLIC_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' },
{ name: 'NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' },
],
bridgedTokens: [
{
......@@ -29,6 +29,10 @@ export const featureEnvs = {
value: '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]',
},
],
zkRollup: [
{ name: 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
],
};
export const viewsEnvs = {
......
......@@ -50,4 +50,12 @@ export const TX: Transaction = {
tx_tag: null,
};
export const TX_ZKEVM_L2: Transaction = {
...TX,
zkevm_batch_number: 12345,
zkevm_sequence_hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc',
zkevm_status: 'Confirmed by Sequencer',
zkevm_verify_hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc',
};
export const TX_RAW_TRACE: RawTracesResponse = [];
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import { TX_HASH } from './tx';
export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = {
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Finalized',
verify_tx_hash: TX_HASH,
sequence_tx_hash: TX_HASH,
number: 5218590,
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,8 +65,15 @@ export type Transaction = {
validator_address: AddressParam;
validator_fee: string;
};
// zkEvm fields
zkevm_verify_hash?: string;
zkevm_batch_number?: number;
zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number];
zkevm_sequence_hash?: string;
}
export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ];
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
export interface TransactionsResponseValidated {
......
import type { Transaction } from './transaction';
export type ZkEvmL2TxnBatchesItem = {
number: number;
verify_tx_hash: string | null;
sequence_tx_hash: string | null;
status: string;
timestamp: string;
tx_count: number;
}
export type ZkEvmL2TxnBatchesResponse = {
items: Array<ZkEvmL2TxnBatchesItem>;
next_page_params: {
number: number;
items_count: number;
} | null;
}
export const ZKEVM_L2_TX_BATCH_STATUSES = [ 'Unfinalized', 'L1 Sequence Confirmed', 'Finalized' ];
export type ZkEvmL2TxnBatch = {
acc_input_hash: string;
global_exit_root: string;
number: number;
sequence_tx_hash: string;
state_root: string;
status: typeof ZKEVM_L2_TX_BATCH_STATUSES[number];
timestamp: string;
transactions: Array<string>;
verify_tx_hash: string;
}
export type ZkEvmL2TxnBatchTxs = {
items: Array<Transaction>;
// API responce doesn't have next_page_params option, but we need to add it to the type for consistency
next_page_params: null;
}
export type NewZkEvmBatchSocketResponse = { batch: ZkEvmL2TxnBatchesItem };
......@@ -14,7 +14,7 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean };
......
......@@ -13,7 +13,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }
......
......@@ -38,6 +38,8 @@ interface Props {
query: UseQueryResult<Block, ResourceError>;
}
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlockDetails = ({ query }: Props) => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
......@@ -87,7 +89,7 @@ const BlockDetails = ({ query }: Props) => {
const validatorTitle = getNetworkValidatorTitle();
const rewardBreakDown = (() => {
if (config.features.rollup.isEnabled || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) {
if (isRollup || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) {
return null;
}
......@@ -120,6 +122,14 @@ const BlockDetails = ({ query }: Props) => {
);
})();
const verificationTitle = (() => {
if (config.features.zkEvmRollup.isEnabled) {
return 'Sequenced by';
}
return config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by';
})();
return (
<Grid
columnGap={ 8 }
......@@ -193,7 +203,7 @@ const BlockDetails = ({ query }: Props) => {
</DetailsInfoItem>
) }
<DetailsInfoItem
title={ config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
title={ verificationTitle }
hint="A block producer who successfully included the block onto the blockchain"
columnGap={ 1 }
isLoading={ isPlaceholderData }
......@@ -205,7 +215,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
{ !config.features.rollup.isEnabled && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && (
{ !isRollup && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && (
<DetailsInfoItem
title="Block reward"
hint={
......
......@@ -28,6 +28,8 @@ interface Props {
enableTimeIncrement?: boolean;
}
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
......@@ -89,7 +91,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
) }
</Flex>
</Box>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { config.chain.currency.symbol }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
......@@ -97,7 +99,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton>
</Flex>
) }
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && (
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
......
......@@ -26,13 +26,15 @@ const GAS_COL_WEIGHT = 33;
const REWARD_COL_WEIGHT = 22;
const FEES_COL_WEIGHT = 22;
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, socketInfoAlert }: Props) => {
const widthBase =
VALIDATOR_COL_WEIGHT +
GAS_COL_WEIGHT +
(!config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward ? REWARD_COL_WEIGHT : 0) +
(!config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees ? FEES_COL_WEIGHT : 0);
(!isRollup && !config.UI.views.block.hiddenFields?.total_reward ? REWARD_COL_WEIGHT : 0) +
(!isRollup && !config.UI.views.block.hiddenFields?.burnt_fees ? FEES_COL_WEIGHT : 0);
return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
......@@ -43,9 +45,9 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
<Th width={ `${ VALIDATOR_COL_WEIGHT / widthBase * 100 }%` } minW="160px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width={ `${ GAS_COL_WEIGHT / widthBase * 100 }%` }>Gas used</Th>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward &&
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward &&
<Th width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { config.chain.currency.symbol }</Th> }
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees &&
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees &&
<Th width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { config.chain.currency.symbol }</Th> }
</Tr>
</Thead>
......
......@@ -26,6 +26,8 @@ interface Props {
enableTimeIncrement?: boolean;
}
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
......@@ -82,7 +84,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton>
) : data.tx_count }
</Td>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }>
......@@ -109,7 +111,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && (
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 2 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
......
......@@ -42,7 +42,7 @@ test('default view +@mobile +@dark-mode', async({ mount, page }) => {
const testL2 = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
testL2('L2 view', async({ mount, page }) => {
......
......@@ -24,7 +24,7 @@ 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) {
if (config.features.optimisticRollup.isEnabled || config.UI.views.block.hiddenFields?.total_reward) {
blocksMaxCount = isMobile ? 4 : 5;
} else {
blocksMaxCount = isMobile ? 2 : 3;
......
......@@ -59,14 +59,14 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
<Skeleton isLoaded={ !isLoading }>Txn</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ block.tx_count }</span></Skeleton>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
{ !config.features.optimisticRollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
<>
<Skeleton isLoaded={ !isLoading }>Reward</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ totalReward.dp(10).toFixed() }</span></Skeleton>
</>
) }
{ !config.features.rollup.isEnabled && (
{ !config.features.optimisticRollup.isEnabled && (
<>
<Skeleton isLoaded={ !isLoading } textTransform="capitalize">{ getNetworkValidatorTitle() }</Skeleton>
<AddressEntity
......
......@@ -11,7 +11,7 @@ import LatestDeposits from './LatestDeposits';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
......
......@@ -15,7 +15,7 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = {
item: L2DepositsItem;
......
......@@ -17,9 +17,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Icon from 'ui/shared/chakra/Icon';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......
......@@ -16,9 +16,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Icon from 'ui/shared/chakra/Icon';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches';
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 LatestZkEvmL2Batches from './LatestZkEvmL2Batches';
const BATCHES_API_URL = buildApiUrl('homepage_zkevm_l2_batches');
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkRollup) as any,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(BATCHES_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txnBatchesData),
}));
const component = await mount(
<TestApp>
<LatestZkEvmL2Batches/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Heading, Flex, Text, VStack } 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 { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import { route } from 'nextjs-routes';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ZKEVM_L2_TXN_BATCHES_ITEM } from 'stubs/zkEvmL2';
import LinkInternal from 'ui/shared/LinkInternal';
import LatestZkevmL2BatchItem from './LatestZkevmL2BatchItem';
const LatestZkEvmL2Batches = () => {
const isMobile = useIsMobile();
const batchesMaxCount = isMobile ? 2 : 5;
const queryClient = useQueryClient();
const { data, isPlaceholderData, isError } = useApiQuery('homepage_zkevm_l2_batches', {
queryOptions: {
placeholderData: { items: Array(batchesMaxCount).fill(ZKEVM_L2_TXN_BATCHES_ITEM) },
},
});
const handleNewBatchMessage: SocketMessage.NewZkEvmL2Batch['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_zkevm_l2_batches'), (prevData: { items: Array<ZkEvmL2TxnBatchesItem> } | undefined) => {
const newItems = prevData?.items ? [ ...prevData.items ] : [];
if (newItems.some((batch => batch.number === payload.batch.number))) {
return { items: newItems };
}
return { items: [ payload.batch, ...newItems ].sort((b1, b2) => b2.number - b1.number).slice(0, batchesMaxCount) };
});
}, [ queryClient, batchesMaxCount ]);
const channel = useSocketChannel({
topic: 'zkevm_batches:new_zkevm_confirmed_batch',
isDisabled: isPlaceholderData || isError,
});
useSocketMessage({
channel,
event: 'new_zkevm_confirmed_batch',
handler: handleNewBatchMessage,
});
let content;
if (isError) {
content = <Text>No data. Please reload page.</Text>;
}
if (data) {
const dataToShow = data.items.slice(0, batchesMaxCount);
content = (
<>
<VStack spacing={ 3 } mb={ 4 } overflow="hidden" alignItems="stretch">
<AnimatePresence initial={ false } >
{ dataToShow.map(((batch, index) => (
<LatestZkevmL2BatchItem
key={ batch.number + (isPlaceholderData ? String(index) : '') }
batch={ batch }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ route({ pathname: '/zkevm-l2-txn-batches' }) }>View all batches</LinkInternal>
</Flex>
</>
);
}
return (
<Box width={{ base: '100%', lg: '280px' }} flexShrink={ 0 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest batches</Heading>
{ content }
</Box>
);
};
export default LatestZkEvmL2Batches;
import {
Box,
Flex,
Skeleton,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import { route } from 'nextjs-routes';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import LinkInternal from 'ui/shared/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
type Props = {
batch: ZkEvmL2TxnBatchesItem;
isLoading?: boolean;
}
const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
return (
<Box
as={ motion.div }
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ display: 'none' }}
transitionDuration="normal"
transitionTimingFunction="linear"
borderRadius="md"
border="1px solid"
borderColor="divider"
p={ 6 }
>
<Flex alignItems="center" overflow="hidden" w="100%" mb={ 3 }>
<ZkEvmBatchEntityL2
isLoading={ isLoading }
number={ batch.number }
tailLength={ 2 }
fontSize="xl"
lineHeight={ 7 }
fontWeight={ 500 }
mr="auto"
/>
<BlockTimestamp
ts={ batch.timestamp }
isEnabled={ !isLoading }
isLoading={ isLoading }
fontSize="sm"
flexShrink={ 0 }
ml={ 2 }
/>
</Flex>
<Flex alignItems="center" justifyContent="space-between" w="100%" flexWrap="wrap">
<Flex alignItems="center">
<Skeleton isLoaded={ !isLoading } mr={ 2 }>Txn</Skeleton>
<LinkInternal
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: batch.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
{ batch.tx_count }
</Skeleton>
</LinkInternal>
</Flex>
<ZkEvmL2TxnBatchStatus status={ batch.status } isLoading={ isLoading }/>
</Flex>
</Box>
);
};
export default LatestZkevmL2BatchItem;
......@@ -10,6 +10,7 @@ import clockIcon from 'icons/clock-light.svg';
import bitcoinIcon from 'icons/coins/bitcoin.svg';
import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg';
import batchesIcon from 'icons/txn_batches.svg';
import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
......@@ -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;
}
......@@ -48,6 +56,15 @@ const Stats = () => {
content = (
<>
{ config.features.zkEvmRollup.isEnabled ? (
<StatsItem
icon={ batchesIcon }
title="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/zkevm-l2-txn-batches' }) }
isLoading={ zkEvmLatestBatchQuery.isPlaceholderData }
/>
) : (
<StatsItem
icon={ blockIcon }
title="Total blocks"
......@@ -55,6 +72,7 @@ const Stats = () => {
url={ route({ pathname: '/blocks' }) }
isLoading={ isPlaceholderData }
/>
) }
{ hasAvgBlockTime && (
<StatsItem
icon={ clockIcon }
......
......@@ -10,10 +10,10 @@ import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
const TransactionsHome = () => {
const hasAccount = useHasAccount();
if (config.features.rollup.isEnabled || hasAccount) {
if (config.features.optimisticRollup.isEnabled || hasAccount) {
const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
config.features.rollup.isEnabled && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
config.features.optimisticRollup.isEnabled && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean);
return (
......
......@@ -12,7 +12,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2DepositsItem; isLoading?: boolean };
......
......@@ -11,7 +11,7 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2DepositsItem; isLoading?: boolean };
......
......@@ -11,7 +11,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean };
......
......@@ -10,7 +10,7 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean };
......
......@@ -13,7 +13,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
......
......@@ -12,7 +12,7 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
......
......@@ -11,7 +11,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
......
......@@ -10,7 +10,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
......
......@@ -4,6 +4,7 @@ import React from 'react';
import config from 'configs/app';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches';
import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner';
......@@ -43,7 +44,7 @@ const Home = () => {
<ChainIndicators/>
<AdBanner mt={{ base: 6, lg: 8 }} mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/>
{ config.features.zkEvmRollup.isEnabled ? <LatestZkEvmL2Batches/> : <LatestBlocks/> }
<Box flexGrow={ 1 }>
<Transactions/>
</Box>
......
......@@ -14,7 +14,7 @@ const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count');
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
test('base view +@mobile', async({ mount, page }) => {
......
......@@ -11,7 +11,7 @@ import OutputRoots from './L2OutputRoots';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
const OUTPUT_ROOTS_API_URL = buildApiUrl('l2_output_roots');
......
......@@ -11,7 +11,7 @@ import L2TxnBatches from './L2TxnBatches';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
const TXN_BATCHES_API_URL = buildApiUrl('l2_txn_batches');
......
......@@ -11,7 +11,7 @@ import L2Withdrawals from './L2Withdrawals';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
const WITHDRAWALS_API_URL = buildApiUrl('l2_withdrawals');
......
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import { txnBatchData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatch';
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 ZkEvmL2TxnBatch from './ZkEvmL2TxnBatch';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkRollup) as any,
});
const hooksConfig = {
router: {
query: { number: '5' },
},
};
const BATCH_API_URL = buildApiUrl('zkevm_l2_txn_batch', { number: '5' });
test('base view', 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(BATCH_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txnBatchData),
}));
const component = await mount(
<TestApp>
<ZkEvmL2TxnBatch/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', 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(BATCH_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txnBatchData),
}));
const component = await mount(
<TestApp>
<ZkEvmL2TxnBatch/>
</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 getQueryParamString from 'lib/router/getQueryParamString';
import { TX_ZKEVM_L2 } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { ZKEVM_L2_TXN_BATCH } from 'stubs/zkEvmL2';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
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';
import ZkEvmL2TxnBatchDetails from 'ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails';
const ZkEvmL2TxnBatch = () => {
const router = useRouter();
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 batchTxsQuery = useQueryWithPages({
resourceName: 'zkevm_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'txs'),
// there is no pagination in zkevm_l2_txn_batch_txs
placeholderData: generateListStub<'zkevm_l2_txn_batch_txs'>(TX_ZKEVM_L2, 50, { next_page_params: null }),
},
});
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: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ batchTxsQuery } showSocketInfo={ false }/> },
].filter(Boolean)), [ batchQuery, batchTxsQuery ]);
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 }
/>
) }
</>
);
};
export default ZkEvmL2TxnBatch;
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches';
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 ZkEvmL2TxnBatches from './ZkEvmL2TxnBatches';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkRollup) as any,
});
const BATCHES_API_URL = buildApiUrl('zkevm_l2_txn_batches');
const BATCHES_COUNTERS_API_URL = buildApiUrl('zkevm_l2_txn_batches_count');
test('base view +@mobile', async({ mount, page }) => {
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(txnBatchesData),
}));
await page.route(BATCHES_COUNTERS_API_URL, (route) => route.fulfill({
status: 200,
body: '9927',
}));
const component = await mount(
<TestApp>
<ZkEvmL2TxnBatches/>
</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 { ZKEVM_L2_TXN_BATCHES_ITEM } from 'stubs/zkEvmL2';
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 ZkEvmTxnBatchesListItem from 'ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesListItem';
import ZkEvmTxnBatchesTable from 'ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTable';
const ZkEvmL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'zkevm_l2_txn_batches',
options: {
placeholderData: generateListStub<'zkevm_l2_txn_batches'>(
ZKEVM_L2_TXN_BATCHES_ITEM,
50,
{
next_page_params: {
items_count: 50,
number: 9045200,
},
},
),
},
});
const countersQuery = useApiQuery('zkevm_l2_txn_batches_count', {
queryOptions: {
placeholderData: 5231746,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ZkEvmTxnBatchesListItem
key={ item.number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }><ZkEvmTxnBatchesTable 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 ZkEvmL2TxnBatches;
......@@ -6,7 +6,7 @@ import Hint from 'ui/shared/Hint';
interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode;
hint: string;
hint?: string;
children: React.ReactNode;
note?: string;
isLoading?: boolean;
......@@ -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 } }}>
<Flex columnGap={ 2 } alignItems="flex-start">
<Hint label={ hint } isLoading={ isLoading }/>
{ hint && <Hint label={ hint } isLoading={ isLoading }/> }
<Skeleton isLoaded={ !isLoading }>
<Text fontWeight={{ base: 700, lg: 500 }}>
{ title }
......
......@@ -17,8 +17,6 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props)
const styleProps: ChakraProps = (() => {
const commonProps = {
fontSize: 'sm',
lineHeight: 5,
display: 'inline-block',
alignItems: 'center',
};
......
......@@ -7,7 +7,7 @@ import config from 'configs/app';
import * as AddressEntity from './AddressEntity';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
const AddressEntityL1 = (props: AddressEntity.EntityProps) => {
if (!feature.isEnabled) {
......
......@@ -8,7 +8,7 @@ import config from 'configs/app';
import * as BlockEntity from './BlockEntity';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
const BlockEntityL1 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
......
......@@ -7,7 +7,7 @@ import txBatchIcon from 'icons/txn_batches_slim.svg';
import * as BlockEntity from './BlockEntity';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup;
const BlockEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
......
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import txBatchIcon from 'icons/txn_batches_slim.svg';
import * as BlockEntity from './BlockEntity';
const feature = config.features.zkEvmRollup;
const ZkEvmBatchEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!feature.isEnabled) {
return null;
}
return (
<BlockEntity.Container className={ props.className }>
<BlockEntity.Icon { ...partsProps } asProp={ txBatchIcon }/>
<BlockEntity.Link
{ ...linkProps }
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: props.number.toString() } }) }
>
<BlockEntity.Content { ...partsProps }/>
</BlockEntity.Link>
</BlockEntity.Container>
);
};
export default chakra(ZkEvmBatchEntityL2);
......@@ -8,7 +8,7 @@ import config from 'configs/app';
import * as TxEntity from './TxEntity';
const feature = config.features.rollup;
const feature = config.features.optimisticRollup.isEnabled ? config.features.optimisticRollup : config.features.zkEvmRollup;
const TxEntityL1 = (props: TxEntity.EntityProps) => {
const partsProps = _omit(props, [ 'className', 'onClick' ]);
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import StatusTag from './StatusTag';
test('ok status', async({ page, mount }) => {
await mount(
<TestApp>
<StatusTag type="ok" text="Test"/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 75, height: 30 } });
});
test('error status', async({ page, mount }) => {
await mount(
<TestApp>
<StatusTag type="error" text="Test"/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 75, height: 30 } });
});
test('pending status', async({ page, mount }) => {
await mount(
<TestApp>
<StatusTag type="pending" text="Test"/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 75, height: 30 } });
});
import { TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg';
import Tag from 'ui/shared/chakra/Tag';
export type StatusTagType = 'ok' | 'error' | 'pending';
export interface Props {
status: Transaction['status'];
type: 'ok' | 'error' | 'pending';
text: string;
errorText?: string | null;
isLoading?: boolean;
}
const TxStatus = ({ status, errorText, isLoading }: Props) => {
let label;
const StatusTag = ({ type, text, errorText, isLoading }: Props) => {
let icon;
let colorScheme;
switch (status) {
switch (type) {
case 'ok':
label = 'Success';
icon = successIcon;
colorScheme = 'green';
break;
case 'error':
label = 'Failed';
icon = errorIcon;
colorScheme = 'red';
break;
case null:
label = 'Pending';
case 'pending':
icon = pendingIcon;
// FIXME: it's not gray on mockups
// need to implement new color scheme or redefine colors here
......@@ -43,10 +40,10 @@ const TxStatus = ({ status, errorText, isLoading }: Props) => {
<Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex" isLoading={ isLoading }>
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
<TagLabel>{ text }</TagLabel>
</Tag>
</Tooltip>
);
};
export default TxStatus;
export default StatusTag;
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
export interface Props {
status: Transaction['status'];
errorText?: string | null;
isLoading?: boolean;
}
const TxStatus = ({ status, errorText, isLoading }: Props) => {
let text;
let type: StatusTagType;
switch (status) {
case 'ok':
text = 'Success';
type = 'ok';
break;
case 'error':
text = 'Failed';
type = 'error';
break;
case null:
text = 'Pending';
type = 'pending';
break;
}
return <StatusTag type={ type } text={ text } errorText={ errorText } isLoading={ isLoading }/>;
};
export default TxStatus;
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
export interface Props {
status: ZkEvmL2TxnBatchesItem['status'];
isLoading?: boolean;
}
const ZkEvmL2TxnBatchStatus = ({ status, isLoading }: Props) => {
let type: StatusTagType;
switch (status) {
case 'L1 Sequence Confirmed':
case 'Finalized':
type = 'ok';
break;
default:
type = 'pending';
break;
}
return <StatusTag type={ type } text={ status } isLoading={ isLoading }/>;
};
export default ZkEvmL2TxnBatchStatus;
import { Text, Icon, HStack } from '@chakra-ui/react';
import React from 'react';
import arrowIcon from 'icons/arrows/east.svg';
import finalizedIcon from 'icons/finalized.svg';
import unfinalizedIcon from 'icons/unfinalized.svg';
type Props = {
step: string;
isLast: boolean;
isPassed: boolean;
}
const VerificationStep = ({ step, isLast, isPassed }: Props) => {
const stepColor = isPassed ? 'green.500' : 'text_secondary';
return (
<HStack gap={ 2 } color={ stepColor }>
<Icon as={ isPassed ? finalizedIcon : unfinalizedIcon } boxSize={ 5 }/>
<Text color={ stepColor }>{ step }</Text>
{ !isLast && <Icon as={ arrowIcon } boxSize={ 5 }/> }
</HStack>
);
};
export default VerificationStep;
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction';
import TestApp from 'playwright/TestApp';
import VerificationSteps from './VerificationSteps';
test('first step +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<Box p={ 10 }>
<VerificationSteps step={ ZKEVM_L2_TX_STATUSES[0] } steps={ ZKEVM_L2_TX_STATUSES }/>
</Box>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('second status', async({ mount }) => {
const component = await mount(
<TestApp>
<VerificationSteps step={ ZKEVM_L2_TX_STATUSES[1] } steps={ ZKEVM_L2_TX_STATUSES }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import VerificationStep from './VerificationStep';
export interface Props<T extends string> {
step: T;
steps: Array<T>;
isLoading?: boolean;
}
const VerificationSteps = <T extends string>({ step, steps, isLoading }: Props<T>) => {
const currentStepIndex = steps.indexOf(step);
return (
<Skeleton
isLoaded={ !isLoading }
display="flex"
gap={ 2 }
alignItems="center"
flexWrap="wrap"
>
{ steps.map((step, index) => (
<VerificationStep step={ step } isLast={ index === steps.length - 1 } isPassed={ index <= currentStepIndex } key={ step }/>
)) }
</Skeleton>
);
};
export default VerificationSteps;
......@@ -40,7 +40,7 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => {
try {
const url = new URL(data.projectWebsite);
return (
<LinkExternal href={ data.projectWebsite } variant="subtle" flexShrink={ 0 }>
<LinkExternal href={ data.projectWebsite } variant="subtle" flexShrink={ 0 } fontSize="sm" lineHeight={ 5 }>
{ url.host }
</LinkExternal>
);
......
......@@ -33,6 +33,8 @@ const Item = ({ data, isLoading }: ItemProps) => {
w="100%"
overflow="hidden"
href={ data.value }
fontSize="sm"
lineHeight={ 5 }
>
<TruncatedValue value={ data.value } w="calc(100% - 16px)"/>
</LinkExternal>
......
......@@ -155,7 +155,7 @@ test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => {
const l2Test = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.rollup) as any,
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
l2Test('l2', async({ mount, page }) => {
......
......@@ -17,6 +17,8 @@ import BigNumber from 'bignumber.js';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction';
import { route } from 'nextjs-routes';
import config from 'configs/app';
......@@ -38,13 +40,15 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TextSeparator from 'ui/shared/TextSeparator';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization/Utilization';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
import TxDetailsActions from 'ui/tx/details/TxDetailsActions';
import TxDetailsFeePerGas from 'ui/tx/details/TxDetailsFeePerGas';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
......@@ -141,7 +145,7 @@ const TxDetails = () => {
<CopyToClipboard text={ data.hash } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Status and method"
title={ config.features.zkEvmRollup.isEnabled ? 'L2 status and method' : 'Status and method' }
hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
isLoading={ isPlaceholderData }
>
......@@ -152,6 +156,15 @@ const TxDetails = () => {
</Tag>
) }
</DetailsInfoItem>
{ data.zkevm_status && (
<DetailsInfoItem
title="Confirmation status"
hint="Status of the transaction confirmation path to L1"
isLoading={ isPlaceholderData }
>
<VerificationSteps step={ data.zkevm_status } steps={ ZKEVM_L2_TX_STATUSES } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
) }
{ data.revert_reason && (
<DetailsInfoItem
title="Revert reason"
......@@ -182,6 +195,18 @@ const TxDetails = () => {
</>
) }
</DetailsInfoItem>
{ data.zkevm_batch_number && (
<DetailsInfoItem
title="Tx batch"
hint="Batch index for this transaction"
isLoading={ isPlaceholderData }
>
<ZkEvmBatchEntityL2
isLoading={ isPlaceholderData }
number={ data.zkevm_batch_number }
/>
</DetailsInfoItem>
) }
{ data.timestamp && (
<DetailsInfoItem
title="Timestamp"
......@@ -286,6 +311,36 @@ const TxDetails = () => {
<DetailsInfoItemDivider/>
{ data.zkevm_sequence_hash && (
<DetailsInfoItem
title="Sequence tx hash"
flexWrap="nowrap"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.zkevm_sequence_hash }/>
</Skeleton>
<CopyToClipboard text={ data.zkevm_sequence_hash } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
) }
{ data.zkevm_verify_hash && (
<DetailsInfoItem
title="Verify tx hash"
flexWrap="nowrap"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.zkevm_verify_hash }/>
</Skeleton>
<CopyToClipboard text={ data.zkevm_verify_hash } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
) }
{ (data.zkevm_batch_number || data.zkevm_verify_hash) && <DetailsInfoItemDivider/> }
{ !config.UI.views.tx.hiddenFields?.value && (
<DetailsInfoItem
title="Value"
......@@ -369,7 +424,7 @@ const TxDetails = () => {
) }
</DetailsInfoItem>
) }
{ data.tx_burnt_fee && !config.UI.views.tx.hiddenFields?.burnt_fees && !config.features.rollup.isEnabled && (
{ data.tx_burnt_fee && !config.UI.views.tx.hiddenFields?.burnt_fees && !config.features.optimisticRollup.isEnabled && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ config.chain.currency.symbol } burned for this transaction. Equals Block Base Fee per Gas * Gas Used` }
......@@ -384,7 +439,7 @@ const TxDetails = () => {
/>
</DetailsInfoItem>
) }
{ config.features.rollup.isEnabled && (
{ config.features.optimisticRollup.isEnabled && (
<>
{ data.l1_gas_used && (
<DetailsInfoItem
......
......@@ -10,7 +10,7 @@ import Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { isLoading?: boolean };
......
......@@ -9,7 +9,7 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxStatus from 'ui/shared/TxStatus';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & {
......
......@@ -6,13 +6,14 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Transaction } from 'types/api/transaction';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import delay from 'lib/delay';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
import { TX, TX_ZKEVM_L2 } from 'stubs/tx';
interface Params {
onTxStatusUpdate?: () => void;
......@@ -34,7 +35,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
queryOptions: {
enabled: Boolean(hash),
refetchOnMount: false,
placeholderData: TX,
placeholderData: config.features.zkEvmRollup.isEnabled ? TX_ZKEVM_L2 : TX,
},
});
const { data, isError, isLoading } = queryResult;
......
......@@ -15,7 +15,8 @@ import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort';
type Props = {
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'>;
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
......
......@@ -19,9 +19,9 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......
......@@ -23,9 +23,9 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from './TxType';
......
import { Grid, Text, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { ZKEVM_L2_TX_BATCH_STATUSES } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2TxnBatches';
import { route } from 'nextjs-routes';
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 CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import TextSeparator from 'ui/shared/TextSeparator';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
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;
}
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"
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"
isLoading={ isPlaceholderData }
>
<VerificationSteps steps={ ZKEVM_L2_TX_BATCH_STATUSES } step={ data.status } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Timestamp"
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"
isLoading={ isPlaceholderData }
>
{ data.verify_tx_hash ? (
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.verify_tx_hash }
maxW="100%"
/>
) : <Text>pending</Text> }
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ data.transactions.length } transaction{ data.transactions.length === 1 ? '' : 's' }
</LinkInternal>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItemDivider/>
<DetailsInfoItem
title="Global exit root"
isLoading={ isPlaceholderData }
flexWrap="nowrap"
>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.global_exit_root }/>
</Skeleton>
<CopyToClipboard text={ data.global_exit_root } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Acc input hash"
isLoading={ isPlaceholderData }
flexWrap="nowrap"
>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.acc_input_hash }/>
</Skeleton>
<CopyToClipboard text={ data.acc_input_hash } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Sequence tx hash"
isLoading={ isPlaceholderData }
>
{ data.sequence_tx_hash ? (
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.sequence_tx_hash }
maxW="100%"
/>
) : <Text>Pending</Text> }
</DetailsInfoItem>
<DetailsInfoItem
title="State root"
isLoading={ isPlaceholderData }
flexWrap="nowrap"
>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.state_root }/>
</Skeleton>
<CopyToClipboard text={ data.state_root } isLoading={ isPlaceholderData }/>
</DetailsInfoItem>
</Grid>
);
};
export default ZkEvmL2TxnBatchDetails;
import { Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
const feature = config.features.zkEvmRollup;
type Props = { item: ZkEvmL2TxnBatchesItem; isLoading?: boolean };
const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<ListItemMobileGrid.Container gridTemplateColumns="110px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ZkEvmBatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ZkEvmL2TxnBatchStatus 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: '/zkevm-l2-txn-batch/[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 }>Verify Tx Has</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.verify_tx_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.verify_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
/>
) : <Text>Pending</Text> }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Sequence hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.sequence_tx_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.sequence_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
/>
) : <Text>Pending</Text> }
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ZkEvmTxnBatchesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import { default as Thead } from 'ui/shared/TheadSticky';
import ZkEvmTxnBatchesTableItem from './ZkEvmTxnBatchesTableItem';
type Props = {
items: Array<ZkEvmL2TxnBatchesItem>;
top: number;
isLoading?: boolean;
}
const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Thead top={ top }>
<Tr>
<Th width="170px">Batch #</Th>
<Th width="150px">Status</Th>
<Th width="150px">Age</Th>
<Th width="170px">Txn count</Th>
<Th width="50%">Verify Tx Has</Th>
<Th width="50%">Sequence hash</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ZkEvmTxnBatchesTableItem
key={ item.number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default TxnBatchesTable;
import { Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
const feature = config.features.zkEvmRollup;
type Props = { item: ZkEvmL2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<Tr>
<Td verticalAlign="middle">
<ZkEvmBatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</Td>
<Td verticalAlign="middle">
<ZkEvmL2TxnBatchStatus 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: '/zkevm-l2-txn-batch/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
</Skeleton>
</LinkInternal>
</Td>
<Td pr={ 12 } verticalAlign="middle">
{ item.verify_tx_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.verify_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
/>
) : <Text>Pending</Text> }
</Td>
<Td pr={ 12 } verticalAlign="middle">
{ item.sequence_tx_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.sequence_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
/>
) : <Text>Pending</Text> }
</Td>
</Tr>
);
};
export default TxnBatchesTableItem;
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