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'; ...@@ -11,7 +11,7 @@ export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as marketplace } from './marketplace'; export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel'; export { default as mixpanel } from './mixpanel';
export { default as restApiDocs } from './restApiDocs'; 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 safe } from './safe';
export { default as sentry } from './sentry'; export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml'; export { default as sol2uml } from './sol2uml';
...@@ -19,3 +19,4 @@ export { default as stats } from './stats'; ...@@ -19,3 +19,4 @@ export { default as stats } from './stats';
export { default as suave } from './suave'; export { default as suave } from './suave';
export { default as web3Wallet } from './web3Wallet'; export { default as web3Wallet } from './web3Wallet';
export { default as verifiedTokens } from './verifiedTokens'; export { default as verifiedTokens } from './verifiedTokens';
export { default as zkEvmRollup } from './zkEvmRollup';
...@@ -6,10 +6,10 @@ const title = 'Rollup (L2) chain'; ...@@ -6,10 +6,10 @@ const title = 'Rollup (L2) chain';
const config: Feature<{ L1BaseUrl: string; withdrawalUrl: string }> = (() => { const config: Feature<{ L1BaseUrl: string; withdrawalUrl: string }> = (() => {
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_L1_BASE_URL'); 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 ( if (
getEnvValue('NEXT_PUBLIC_IS_L2_NETWORK') === 'true' && getEnvValue('NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK') === 'true' &&
L1BaseUrl && L1BaseUrl &&
withdrawalUrl 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 ...@@ -44,7 +44,8 @@ NEXT_PUBLIC_APP_INSTANCE=jest
NEXT_PUBLIC_APP_ENV=testing NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form 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_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100 NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout 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 ...@@ -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_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_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-goerli.k8s-dev.blockscout.com 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_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 ...@@ -40,7 +40,8 @@ NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_APP_INSTANCE=pw NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form 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_AD_BANNER_PROVIDER=slise
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100 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 ...@@ -106,20 +106,23 @@ const beaconChainSchema = yup
const rollupSchema = yup const rollupSchema = yup
.object() .object()
.shape({ .shape({
NEXT_PUBLIC_IS_L2_NETWORK: yup.boolean(), NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: yup.boolean(),
NEXT_PUBLIC_L1_BASE_URL: yup NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: yup
.string() .string()
.when('NEXT_PUBLIC_IS_L2_NETWORK', { .when('NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', {
is: (value: boolean) => value, is: (value: string) => value,
then: (schema) => schema.test(urlTest).required(), 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() .string()
.when('NEXT_PUBLIC_IS_L2_NETWORK', { .when([ 'NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK' ], {
is: (value: string) => value, is: (isOptimistic?: boolean, isZk?: boolean) => isOptimistic || isZk,
then: (schema) => schema.test(urlTest).required(), 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_L1_BASE_URL=https://example.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL=https://example.com NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://example.com
\ No newline at end of file \ No newline at end of file
...@@ -190,9 +190,9 @@ frontend: ...@@ -190,9 +190,9 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer-test.k8s-dev.blockscout.com 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_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_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_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 NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
envFromSecret: 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 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: ...@@ -104,8 +104,6 @@ frontend:
_default: https://stats-optimism-goerli.k8s-dev.blockscout.com _default: https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: NEXT_PUBLIC_MARKETPLACE_CONFIG_URL:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json _default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: ''
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: 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%)" _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: NEXT_PUBLIC_NETWORK_RPC_URL:
...@@ -124,12 +122,12 @@ frontend: ...@@ -124,12 +122,12 @@ frontend:
_default: https://contracts-info-test.k8s-dev.blockscout.com _default: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: NEXT_PUBLIC_ADMIN_SERVICE_API_HOST:
_default: https://admin-rs-test.k8s-dev.blockscout.com _default: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_L2_NETWORK: NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK:
_default: "true" _default: "true"
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_L1_BASE_URL: NEXT_PUBLIC_L1_BASE_URL:
_default: https://blockscout-main.k8s-dev.blockscout.com _default: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62 _default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_SENTRY_DSN: NEXT_PUBLIC_SENTRY_DSN:
......
...@@ -33,7 +33,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -33,7 +33,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Banner ads](ENVS.md#banner-ads) - [Banner ads](ENVS.md#banner-ads)
- [Text ads](ENVS.md#text-ads) - [Text ads](ENVS.md#text-ads)
- [Beacon chain](ENVS.md#beacon-chain) - [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) - [Export data to CSV file](ENVS.md#export-data-to-csv-file)
- [Google analytics](ENVS.md#google-analytics) - [Google analytics](ENVS.md#google-analytics)
- [Mixpanel analytics](ENVS.md#mixpanel-analytics) - [Mixpanel analytics](ENVS.md#mixpanel-analytics)
...@@ -337,13 +338,22 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -337,13 +338,22 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp; &nbsp;
### Rollup (L2) chain ### Optimistic rollup (L2) chain
| Variable | Type| Description | Compulsoriness | Default value | Example value | | 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_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; &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 = { ...@@ -17,6 +17,7 @@ const PAGE_PROPS = {
id: '', id: '',
height_or_hash: '', height_or_hash: '',
hash: '', hash: '',
number: '',
q: '', q: '',
}; };
......
...@@ -59,6 +59,7 @@ import type { TTxsFilters } from 'types/api/txsFilters'; ...@@ -59,6 +59,7 @@ import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2TxnBatches';
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
import config from 'configs/app'; import config from 'configs/app';
...@@ -426,12 +427,18 @@ export const RESOURCES = { ...@@ -426,12 +427,18 @@ export const RESOURCES = {
homepage_txs: { homepage_txs: {
path: '/api/v2/main-page/transactions', path: '/api/v2/main-page/transactions',
}, },
homepage_zkevm_l2_batches: {
path: '/api/v2/main-page/zkevm/batches/confirmed',
},
homepage_txs_watchlist: { homepage_txs_watchlist: {
path: '/api/v2/main-page/transactions/watchlist', path: '/api/v2/main-page/transactions/watchlist',
}, },
homepage_indexing_status: { homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status', path: '/api/v2/main-page/indexing-status',
}, },
homepage_zkevm_latest_batch: {
path: '/api/v2/main-page/zkevm/batches/latest-number',
},
// SEARCH // SEARCH
quick_search: { quick_search: {
...@@ -483,6 +490,25 @@ export const RESOURCES = { ...@@ -483,6 +490,25 @@ export const RESOURCES = {
path: '/api/v2/optimism/txn-batches/count', 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 // CONFIGS
config_backend_version: { config_backend_version: {
path: '/api/v2/config/backend-version', path: '/api/v2/config/backend-version',
...@@ -552,6 +578,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -552,6 +578,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals'; 'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -575,7 +602,9 @@ Q extends 'homepage_blocks' ? Array<Block> : ...@@ -575,7 +602,9 @@ Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> : Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<L2DepositsItem> : Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
Q extends 'homepage_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 '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 :
...@@ -640,6 +669,10 @@ Q extends 'l2_output_roots_count' ? number : ...@@ -640,6 +669,10 @@ Q extends 'l2_output_roots_count' ? number :
Q extends 'l2_withdrawals_count' ? number : Q extends 'l2_withdrawals_count' ? number :
Q extends 'l2_deposits_count' ? number : Q extends 'l2_deposits_count' ? number :
Q extends 'l2_txn_batches_count' ? number : Q extends 'l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse :
Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'config_backend_version' ? BackendVersionConfig : Q extends 'config_backend_version' ? BackendVersionConfig :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -13,6 +13,7 @@ const AppContext = createContext<PageProps>({ ...@@ -13,6 +13,7 @@ const AppContext = createContext<PageProps>({
id: '', id: '',
height_or_hash: '', height_or_hash: '',
hash: '', hash: '',
number: '',
q: '', q: '',
}); });
......
...@@ -71,7 +71,20 @@ export default function useNavItems(): ReturnType { ...@@ -71,7 +71,20 @@ export default function useNavItems(): ReturnType {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
{ text: 'Verified contracts', nextRoute: { pathname: '/verified-contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified-contracts' }; { 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 = [ blockchainNavItems = [
[ [
txs, txs,
......
...@@ -36,6 +36,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -36,6 +36,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/l2-output-roots': 'Root page', '/l2-output-roots': 'Root page',
'/l2-txn-batches': 'Root page', '/l2-txn-batches': 'Root page',
'/l2-withdrawals': 'Root page', '/l2-withdrawals': 'Root page',
'/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page',
'/404': 'Regular page', '/404': 'Regular page',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
......
...@@ -39,6 +39,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -39,6 +39,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-output-roots': DEFAULT_TEMPLATE, '/l2-output-roots': DEFAULT_TEMPLATE,
'/l2-txn-batches': DEFAULT_TEMPLATE, '/l2-txn-batches': DEFAULT_TEMPLATE,
'/l2-withdrawals': DEFAULT_TEMPLATE, '/l2-withdrawals': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE, '/404': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
......
...@@ -34,6 +34,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -34,6 +34,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-output-roots': 'output roots', '/l2-output-roots': 'output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)', '/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-withdrawals': 'withdrawals (L2 > L1)', '/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', '/404': 'error - page not found',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
......
...@@ -34,6 +34,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -34,6 +34,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/l2-output-roots': 'Output roots', '/l2-output-roots': 'Output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)', '/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-withdrawals': 'Withdrawals (L2 > L1)', '/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', '/404': '404',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
......
...@@ -6,6 +6,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract'; ...@@ -6,6 +6,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2TxnBatches';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
...@@ -30,6 +31,7 @@ SocketMessage.SmartContractWasVerified | ...@@ -30,6 +31,7 @@ SocketMessage.SmartContractWasVerified |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply | SocketMessage.TokenTotalSupply |
SocketMessage.ContractVerification | SocketMessage.ContractVerification |
SocketMessage.NewZkEvmL2Batch |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -64,5 +66,6 @@ export namespace SocketMessage { ...@@ -64,5 +66,6 @@ export namespace SocketMessage {
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>; export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>; export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>; export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; 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 = { ...@@ -8,6 +8,7 @@ export type Props = {
id: string; id: string;
height_or_hash: string; height_or_hash: string;
hash: string; hash: string;
number: string;
q: string; q: string;
} }
...@@ -19,6 +20,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => { ...@@ -19,6 +20,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
id: query.id?.toString() || '', id: query.id?.toString() || '',
hash: query.hash?.toString() || '', hash: query.hash?.toString() || '',
height_or_hash: query.height_or_hash?.toString() || '', height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '',
q: query.q?.toString() || '', q: query.q?.toString() || '',
}, },
}; };
...@@ -55,7 +57,17 @@ export const beaconChain: GetServerSideProps<Props> = async(context) => { ...@@ -55,7 +57,17 @@ export const beaconChain: GetServerSideProps<Props> = async(context) => {
}; };
export const L2: 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 { return {
notFound: true, notFound: true,
}; };
......
...@@ -46,7 +46,9 @@ declare module "nextjs-routes" { ...@@ -46,7 +46,9 @@ declare module "nextjs-routes" {
| StaticRoute<"/txs"> | StaticRoute<"/txs">
| StaticRoute<"/verified-contracts"> | StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml"> | StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">; | StaticRoute<"/withdrawals">
| DynamicRoute<"/zkevm-l2-txn-batch/[number]", { "number": string }>
| StaticRoute<"/zkevm-l2-txn-batches">;
interface StaticRoute<Pathname> { interface StaticRoute<Pathname> {
pathname: 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 = { ...@@ -27,6 +27,7 @@ const defaultAppContext = {
id: '', id: '',
height_or_hash: '', height_or_hash: '',
hash: '', hash: '',
number: '',
q: '', q: '',
}, },
}; };
......
...@@ -14,10 +14,10 @@ export const featureEnvs = { ...@@ -14,10 +14,10 @@ export const featureEnvs = {
beaconChain: [ beaconChain: [
{ name: 'NEXT_PUBLIC_HAS_BEACON_CHAIN', value: 'true' }, { name: 'NEXT_PUBLIC_HAS_BEACON_CHAIN', value: 'true' },
], ],
rollup: [ optimisticRollup: [
{ name: 'NEXT_PUBLIC_IS_L2_NETWORK', value: 'true' }, { name: 'NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' }, { 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: [ bridgedTokens: [
{ {
...@@ -29,6 +29,10 @@ export const featureEnvs = { ...@@ -29,6 +29,10 @@ export const featureEnvs = {
value: '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]', 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 = { export const viewsEnvs = {
......
...@@ -50,4 +50,12 @@ export const TX: Transaction = { ...@@ -50,4 +50,12 @@ export const TX: Transaction = {
tx_tag: null, 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 = []; 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 = { ...@@ -65,8 +65,15 @@ export type Transaction = {
validator_address: AddressParam; validator_address: AddressParam;
validator_fee: string; validator_fee: string;
}; };
// zkEvm fields
zkevm_verify_hash?: string;
zkevm_batch_number?: number;
zkevm_status?: 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 type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
export interface TransactionsResponseValidated { 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'; ...@@ -14,7 +14,7 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; 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'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }; type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean };
......
...@@ -13,7 +13,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity'; ...@@ -13,7 +13,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import InOutTag from 'ui/shared/InOutTag'; 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'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean } type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }
......
...@@ -38,6 +38,8 @@ interface Props { ...@@ -38,6 +38,8 @@ interface Props {
query: UseQueryResult<Block, ResourceError>; query: UseQueryResult<Block, ResourceError>;
} }
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlockDetails = ({ query }: Props) => { const BlockDetails = ({ query }: Props) => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter(); const router = useRouter();
...@@ -87,7 +89,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -87,7 +89,7 @@ const BlockDetails = ({ query }: Props) => {
const validatorTitle = getNetworkValidatorTitle(); const validatorTitle = getNetworkValidatorTitle();
const rewardBreakDown = (() => { 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; return null;
} }
...@@ -120,6 +122,14 @@ const BlockDetails = ({ query }: Props) => { ...@@ -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 ( return (
<Grid <Grid
columnGap={ 8 } columnGap={ 8 }
...@@ -193,7 +203,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -193,7 +203,7 @@ const BlockDetails = ({ query }: Props) => {
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<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" hint="A block producer who successfully included the block onto the blockchain"
columnGap={ 1 } columnGap={ 1 }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
...@@ -205,7 +215,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -205,7 +215,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ } { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ } { /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem> </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 <DetailsInfoItem
title="Block reward" title="Block reward"
hint={ hint={
......
...@@ -28,6 +28,8 @@ interface Props { ...@@ -28,6 +28,8 @@ interface Props {
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
} }
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
...@@ -89,7 +91,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -89,7 +91,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
) } ) }
</Flex> </Flex>
</Box> </Box>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && ( { !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { config.chain.currency.symbol }</Text> <Text fontWeight={ 500 }>Reward { config.chain.currency.symbol }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary"> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
...@@ -97,7 +99,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -97,7 +99,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton> </Skeleton>
</Flex> </Flex>
) } ) }
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && ( { !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Box> <Box>
<Text fontWeight={ 500 }>Burnt fees</Text> <Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }> <Flex columnGap={ 4 } mt={ 2 }>
......
...@@ -26,13 +26,15 @@ const GAS_COL_WEIGHT = 33; ...@@ -26,13 +26,15 @@ const GAS_COL_WEIGHT = 33;
const REWARD_COL_WEIGHT = 22; const REWARD_COL_WEIGHT = 22;
const FEES_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 BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, socketInfoAlert }: Props) => {
const widthBase = const widthBase =
VALIDATOR_COL_WEIGHT + VALIDATOR_COL_WEIGHT +
GAS_COL_WEIGHT + GAS_COL_WEIGHT +
(!config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward ? REWARD_COL_WEIGHT : 0) + (!isRollup && !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?.burnt_fees ? FEES_COL_WEIGHT : 0);
return ( return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
...@@ -43,9 +45,9 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum ...@@ -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={ `${ VALIDATOR_COL_WEIGHT / widthBase * 100 }%` } minW="160px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th> <Th width="64px" isNumeric>Txn</Th>
<Th width={ `${ GAS_COL_WEIGHT / widthBase * 100 }%` }>Gas used</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> } <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> } <Th width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { config.chain.currency.symbol }</Th> }
</Tr> </Tr>
</Thead> </Thead>
......
...@@ -26,6 +26,8 @@ interface Props { ...@@ -26,6 +26,8 @@ interface Props {
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
} }
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
...@@ -82,7 +84,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -82,7 +84,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton> </Skeleton>
) : data.tx_count } ) : data.tx_count }
</Td> </Td>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && ( { !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Td fontSize="sm"> <Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton> <Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }> <Flex mt={ 2 }>
...@@ -109,7 +111,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -109,7 +111,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
{ totalReward.toFixed(8) } { totalReward.toFixed(8) }
</Skeleton> </Skeleton>
</Td> </Td>
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && ( { !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center" columnGap={ 2 }> <Flex alignItems="center" columnGap={ 2 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/> <Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
......
...@@ -42,7 +42,7 @@ test('default view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -42,7 +42,7 @@ test('default view +@mobile +@dark-mode', async({ mount, page }) => {
const testL2 = test.extend({ const testL2 = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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 }) => { testL2('L2 view', async({ mount, page }) => {
......
...@@ -24,7 +24,7 @@ const LatestBlocks = () => { ...@@ -24,7 +24,7 @@ const LatestBlocks = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// const blocksMaxCount = isMobile ? 2 : 3; // const blocksMaxCount = isMobile ? 2 : 3;
let blocksMaxCount: number; 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; blocksMaxCount = isMobile ? 4 : 5;
} else { } else {
blocksMaxCount = isMobile ? 2 : 3; blocksMaxCount = isMobile ? 2 : 3;
......
...@@ -59,14 +59,14 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => { ...@@ -59,14 +59,14 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
<Skeleton isLoaded={ !isLoading }>Txn</Skeleton> <Skeleton isLoaded={ !isLoading }>Txn</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ block.tx_count }</span></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 }>Reward</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ totalReward.dp(10).toFixed() }</span></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> <Skeleton isLoaded={ !isLoading } textTransform="capitalize">{ getNetworkValidatorTitle() }</Skeleton>
<AddressEntity <AddressEntity
......
...@@ -11,7 +11,7 @@ import LatestDeposits from './LatestDeposits'; ...@@ -11,7 +11,7 @@ import LatestDeposits from './LatestDeposits';
const test = base.extend({ const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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 }) => { test('default view +@mobile +@dark-mode', async({ mount, page }) => {
......
...@@ -15,7 +15,7 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1'; ...@@ -15,7 +15,7 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { type Props = {
item: L2DepositsItem; item: L2DepositsItem;
......
...@@ -17,9 +17,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -17,9 +17,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Icon from 'ui/shared/chakra/Icon'; import Icon from 'ui/shared/chakra/Icon';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
......
...@@ -16,9 +16,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -16,9 +16,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Icon from 'ui/shared/chakra/Icon'; import Icon from 'ui/shared/chakra/Icon';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType'; 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'; ...@@ -10,6 +10,7 @@ import clockIcon from 'icons/clock-light.svg';
import bitcoinIcon from 'icons/coins/bitcoin.svg'; import bitcoinIcon from 'icons/coins/bitcoin.svg';
import gasIcon from 'icons/gas.svg'; import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import batchesIcon from 'icons/txn_batches.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
...@@ -28,7 +29,14 @@ const Stats = () => { ...@@ -28,7 +29,14 @@ const Stats = () => {
}, },
}); });
if (isError) { const zkEvmLatestBatchQuery = useApiQuery('homepage_zkevm_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: config.features.zkEvmRollup.isEnabled,
},
});
if (isError || zkEvmLatestBatchQuery.isError) {
return null; return null;
} }
...@@ -48,13 +56,23 @@ const Stats = () => { ...@@ -48,13 +56,23 @@ const Stats = () => {
content = ( content = (
<> <>
<StatsItem { config.features.zkEvmRollup.isEnabled ? (
icon={ blockIcon } <StatsItem
title="Total blocks" icon={ batchesIcon }
value={ Number(data.total_blocks).toLocaleString() } title="Latest batch"
url={ route({ pathname: '/blocks' }) } value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
isLoading={ isPlaceholderData } url={ route({ pathname: '/zkevm-l2-txn-batches' }) }
/> isLoading={ zkEvmLatestBatchQuery.isPlaceholderData }
/>
) : (
<StatsItem
icon={ blockIcon }
title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) }
isLoading={ isPlaceholderData }
/>
) }
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsItem
icon={ clockIcon } icon={ clockIcon }
......
...@@ -10,10 +10,10 @@ import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; ...@@ -10,10 +10,10 @@ import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
const TransactionsHome = () => { const TransactionsHome = () => {
const hasAccount = useHasAccount(); const hasAccount = useHasAccount();
if (config.features.rollup.isEnabled || hasAccount) { if (config.features.optimisticRollup.isEnabled || hasAccount) {
const tabs = [ const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> }, { 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/> }, hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean); ].filter(Boolean);
return ( return (
......
...@@ -12,7 +12,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity'; ...@@ -12,7 +12,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2DepositsItem; isLoading?: boolean }; type Props = { item: L2DepositsItem; isLoading?: boolean };
......
...@@ -11,7 +11,7 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1'; ...@@ -11,7 +11,7 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2DepositsItem; isLoading?: boolean }; type Props = { item: L2DepositsItem; isLoading?: boolean };
......
...@@ -11,7 +11,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; ...@@ -11,7 +11,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean }; type Props = { item: L2OutputRootsItem; isLoading?: boolean };
......
...@@ -10,7 +10,7 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2'; ...@@ -10,7 +10,7 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean }; type Props = { item: L2OutputRootsItem; isLoading?: boolean };
......
...@@ -13,7 +13,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; ...@@ -13,7 +13,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
......
...@@ -12,7 +12,7 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2'; ...@@ -12,7 +12,7 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
......
...@@ -11,7 +11,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; ...@@ -11,7 +11,7 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean }; type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
......
...@@ -10,7 +10,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity'; ...@@ -10,7 +10,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean }; type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import ChainIndicators from 'ui/home/indicators/ChainIndicators'; import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks'; import LatestBlocks from 'ui/home/LatestBlocks';
import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches';
import Stats from 'ui/home/Stats'; import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions'; import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
...@@ -43,7 +44,7 @@ const Home = () => { ...@@ -43,7 +44,7 @@ const Home = () => {
<ChainIndicators/> <ChainIndicators/>
<AdBanner mt={{ base: 6, lg: 8 }} mx="auto" display="flex" justifyContent="center"/> <AdBanner mt={{ base: 6, lg: 8 }} mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }> <Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/> { config.features.zkEvmRollup.isEnabled ? <LatestZkEvmL2Batches/> : <LatestBlocks/> }
<Box flexGrow={ 1 }> <Box flexGrow={ 1 }>
<Transactions/> <Transactions/>
</Box> </Box>
......
...@@ -14,7 +14,7 @@ const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count'); ...@@ -14,7 +14,7 @@ const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count');
const test = base.extend({ const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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 }) => { test('base view +@mobile', async({ mount, page }) => {
......
...@@ -11,7 +11,7 @@ import OutputRoots from './L2OutputRoots'; ...@@ -11,7 +11,7 @@ import OutputRoots from './L2OutputRoots';
const test = base.extend({ const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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'); const OUTPUT_ROOTS_API_URL = buildApiUrl('l2_output_roots');
......
...@@ -11,7 +11,7 @@ import L2TxnBatches from './L2TxnBatches'; ...@@ -11,7 +11,7 @@ import L2TxnBatches from './L2TxnBatches';
const test = base.extend({ const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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'); const TXN_BATCHES_API_URL = buildApiUrl('l2_txn_batches');
......
...@@ -11,7 +11,7 @@ import L2Withdrawals from './L2Withdrawals'; ...@@ -11,7 +11,7 @@ import L2Withdrawals from './L2Withdrawals';
const test = base.extend({ const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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'); 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'; ...@@ -6,7 +6,7 @@ import Hint from 'ui/shared/Hint';
interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> { interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode; title: React.ReactNode;
hint: string; hint?: string;
children: React.ReactNode; children: React.ReactNode;
note?: string; note?: string;
isLoading?: boolean; isLoading?: boolean;
...@@ -17,7 +17,7 @@ const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles ...@@ -17,7 +17,7 @@ const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles
<> <>
<GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="flex-start"> <Flex columnGap={ 2 } alignItems="flex-start">
<Hint label={ hint } isLoading={ isLoading }/> { hint && <Hint label={ hint } isLoading={ isLoading }/> }
<Skeleton isLoaded={ !isLoading }> <Skeleton isLoaded={ !isLoading }>
<Text fontWeight={{ base: 700, lg: 500 }}> <Text fontWeight={{ base: 700, lg: 500 }}>
{ title } { title }
......
...@@ -17,8 +17,6 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props) ...@@ -17,8 +17,6 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props)
const styleProps: ChakraProps = (() => { const styleProps: ChakraProps = (() => {
const commonProps = { const commonProps = {
fontSize: 'sm',
lineHeight: 5,
display: 'inline-block', display: 'inline-block',
alignItems: 'center', alignItems: 'center',
}; };
......
...@@ -7,7 +7,7 @@ import config from 'configs/app'; ...@@ -7,7 +7,7 @@ import config from 'configs/app';
import * as AddressEntity from './AddressEntity'; import * as AddressEntity from './AddressEntity';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
const AddressEntityL1 = (props: AddressEntity.EntityProps) => { const AddressEntityL1 = (props: AddressEntity.EntityProps) => {
if (!feature.isEnabled) { if (!feature.isEnabled) {
......
...@@ -8,7 +8,7 @@ import config from 'configs/app'; ...@@ -8,7 +8,7 @@ import config from 'configs/app';
import * as BlockEntity from './BlockEntity'; import * as BlockEntity from './BlockEntity';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
const BlockEntityL1 = (props: BlockEntity.EntityProps) => { const BlockEntityL1 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]); const linkProps = _omit(props, [ 'className' ]);
......
...@@ -7,7 +7,7 @@ import txBatchIcon from 'icons/txn_batches_slim.svg'; ...@@ -7,7 +7,7 @@ import txBatchIcon from 'icons/txn_batches_slim.svg';
import * as BlockEntity from './BlockEntity'; import * as BlockEntity from './BlockEntity';
const feature = config.features.rollup; const feature = config.features.optimisticRollup;
const BlockEntityL2 = (props: BlockEntity.EntityProps) => { const BlockEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]); 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'; ...@@ -8,7 +8,7 @@ import config from 'configs/app';
import * as TxEntity from './TxEntity'; 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 TxEntityL1 = (props: TxEntity.EntityProps) => {
const partsProps = _omit(props, [ 'className', 'onClick' ]); 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 { TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction';
import errorIcon from 'icons/status/error.svg'; import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg'; import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg'; import successIcon from 'icons/status/success.svg';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
export type StatusTagType = 'ok' | 'error' | 'pending';
export interface Props { export interface Props {
status: Transaction['status']; type: 'ok' | 'error' | 'pending';
text: string;
errorText?: string | null; errorText?: string | null;
isLoading?: boolean; isLoading?: boolean;
} }
const TxStatus = ({ status, errorText, isLoading }: Props) => { const StatusTag = ({ type, text, errorText, isLoading }: Props) => {
let label;
let icon; let icon;
let colorScheme; let colorScheme;
switch (status) { switch (type) {
case 'ok': case 'ok':
label = 'Success';
icon = successIcon; icon = successIcon;
colorScheme = 'green'; colorScheme = 'green';
break; break;
case 'error': case 'error':
label = 'Failed';
icon = errorIcon; icon = errorIcon;
colorScheme = 'red'; colorScheme = 'red';
break; break;
case null: case 'pending':
label = 'Pending';
icon = pendingIcon; icon = pendingIcon;
// FIXME: it's not gray on mockups // FIXME: it's not gray on mockups
// need to implement new color scheme or redefine colors here // need to implement new color scheme or redefine colors here
...@@ -43,10 +40,10 @@ const TxStatus = ({ status, errorText, isLoading }: Props) => { ...@@ -43,10 +40,10 @@ const TxStatus = ({ status, errorText, isLoading }: Props) => {
<Tooltip label={ errorText }> <Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex" isLoading={ isLoading }> <Tag colorScheme={ colorScheme } display="inline-flex" isLoading={ isLoading }>
<TagLeftIcon boxSize={ 2.5 } as={ icon }/> <TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel> <TagLabel>{ text }</TagLabel>
</Tag> </Tag>
</Tooltip> </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;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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