diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 1b7acd949b..ad45f9a4a1 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -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'; diff --git a/configs/app/features/rollup.ts b/configs/app/features/optimisticRollup.ts similarity index 77% rename from configs/app/features/rollup.ts rename to configs/app/features/optimisticRollup.ts index 76954ce904..6b4f646d59 100644 --- a/configs/app/features/rollup.ts +++ b/configs/app/features/optimisticRollup.ts @@ -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 ) { diff --git a/configs/app/features/zkEvmRollup.ts b/configs/app/features/zkEvmRollup.ts new file mode 100644 index 0000000000..7fda84b09f --- /dev/null +++ b/configs/app/features/zkEvmRollup.ts @@ -0,0 +1,25 @@ +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; diff --git a/configs/envs/.env.jest b/configs/envs/.env.jest index b3a01eb08f..7a14a10b01 100644 --- a/configs/envs/.env.jest +++ b/configs/envs/.env.jest @@ -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 diff --git a/configs/envs/.env.main.L2 b/configs/envs/.env.main.L2 index bbb72abfff..fb9359be95 100644 --- a/configs/envs/.env.main.L2 +++ b/configs/envs/.env.main.L2 @@ -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 diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw index 5daff678fe..cdd8edce29 100644 --- a/configs/envs/.env.pw +++ b/configs/envs/.env.pw @@ -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 diff --git a/configs/envs/.env.zkevm b/configs/envs/.env.zkevm new file mode 100644 index 0000000000..91d7172d3f --- /dev/null +++ b/configs/envs/.env.zkevm @@ -0,0 +1,48 @@ +# 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 diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index cad3a3cd24..c327e40b1f 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -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"'), }), }); diff --git a/deploy/tools/envs-validator/test/.env.rollup b/deploy/tools/envs-validator/test/.env.rollup index cfae86c8a2..8534221d99 100644 --- a/deploy/tools/envs-validator/test/.env.rollup +++ b/deploy/tools/envs-validator/test/.env.rollup @@ -1,3 +1,3 @@ -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 diff --git a/deploy/values/l2-optimism-goerli/values.yaml b/deploy/values/l2-optimism-goerli/values.yaml index 83e3cdf3c7..1d695580d4 100644 --- a/deploy/values/l2-optimism-goerli/values.yaml +++ b/deploy/values/l2-optimism-goerli/values.yaml @@ -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 diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl index 8d0b23d528..42f51f08c6 100644 --- a/deploy/values/review-l2/values.yaml.gotmpl +++ b/deploy/values/review-l2/values.yaml.gotmpl @@ -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: diff --git a/docs/ENVS.md b/docs/ENVS.md index 6c3617261f..3926721855 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -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   -### 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` | + +  + +### 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'` | +   diff --git a/icons/finalized.svg b/icons/finalized.svg new file mode 100644 index 0000000000..fbae66d631 --- /dev/null +++ b/icons/finalized.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/unfinalized.svg b/icons/unfinalized.svg new file mode 100644 index 0000000000..49fdbc7ddf --- /dev/null +++ b/icons/unfinalized.svg @@ -0,0 +1,3 @@ + + + diff --git a/jest/lib.tsx b/jest/lib.tsx index 2602e62861..2c590932cf 100644 --- a/jest/lib.tsx +++ b/jest/lib.tsx @@ -17,6 +17,7 @@ const PAGE_PROPS = { id: '', height_or_hash: '', hash: '', + number: '', q: '', }; diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 68ca913f74..795d97b0e1 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -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 = ResourcePayload; @@ -575,7 +602,9 @@ Q extends 'homepage_blocks' ? Array : Q extends 'homepage_txs' ? Array : Q extends 'homepage_txs_watchlist' ? Array : Q extends 'homepage_deposits' ? Array : +Q extends 'homepage_zkevm_l2_batches' ? { items: Array } : 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 */ diff --git a/lib/contexts/app.tsx b/lib/contexts/app.tsx index 509b86fc1c..f0a1eec14e 100644 --- a/lib/contexts/app.tsx +++ b/lib/contexts/app.tsx @@ -13,6 +13,7 @@ const AppContext = createContext({ id: '', height_or_hash: '', hash: '', + number: '', q: '', }); diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index 46bcd5bca2..4a6e2552ae 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -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, diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 91e1c9f234..f575ecad59 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -36,6 +36,8 @@ const OG_TYPE_DICT: Record = { '/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 diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index a8a5b5d93f..a90525adc7 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -39,6 +39,8 @@ const TEMPLATE_MAP: Record = { '/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 diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index b16abee393..aec42fb6e9 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -34,6 +34,8 @@ const TEMPLATE_MAP: Record = { '/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 diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index 02d1e11c6e..d09caee670 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -34,6 +34,8 @@ export const PAGE_TYPE_DICT: Record = { '/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 diff --git a/lib/socket/types.ts b/lib/socket/types.ts index ebc220fe46..4d05a1acb7 100644 --- a/lib/socket/types.ts +++ b/lib/socket/types.ts @@ -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 { @@ -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; } diff --git a/mocks/zkevmL2txnBatches/zkevmL2txnBatch.ts b/mocks/zkevmL2txnBatches/zkevmL2txnBatch.ts new file mode 100644 index 0000000000..172481ce60 --- /dev/null +++ b/mocks/zkevmL2txnBatches/zkevmL2txnBatch.ts @@ -0,0 +1,15 @@ +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', +}; diff --git a/mocks/zkevmL2txnBatches/zkevmL2txnBatches.ts b/mocks/zkevmL2txnBatches/zkevmL2txnBatches.ts new file mode 100644 index 0000000000..5d81a72686 --- /dev/null +++ b/mocks/zkevmL2txnBatches/zkevmL2txnBatches.ts @@ -0,0 +1,26 @@ +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, + }, +}; diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index 31872b8278..2f62492617 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -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 = 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 = async(context) => { }; export const L2: GetServerSideProps = async(context) => { - if (!config.features.rollup.isEnabled) { + if (!config.features.optimisticRollup.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const zkEvmL2: GetServerSideProps = async(context) => { + if (!config.features.zkEvmRollup.isEnabled) { return { notFound: true, }; diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index a7f05b9560..92bc5e56f9 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -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; diff --git a/pages/zkevm-l2-txn-batch/[number].tsx b/pages/zkevm-l2-txn-batch/[number].tsx new file mode 100644 index 0000000000..b128b8d070 --- /dev/null +++ b/pages/zkevm-l2-txn-batch/[number].tsx @@ -0,0 +1,20 @@ +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) => { + return ( + + + + ); +}; + +export default Page; + +export { zkEvmL2 as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/zkevm-l2-txn-batches.tsx b/pages/zkevm-l2-txn-batches.tsx new file mode 100644 index 0000000000..36b8139216 --- /dev/null +++ b/pages/zkevm-l2-txn-batches.tsx @@ -0,0 +1,19 @@ +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 ( + + + + ); +}; + +export default Page; + +export { zkEvmL2 as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index e08660fc30..fc644a9ad8 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -27,6 +27,7 @@ const defaultAppContext = { id: '', height_or_hash: '', hash: '', + number: '', q: '', }, }; diff --git a/playwright/utils/configs.ts b/playwright/utils/configs.ts index 387a1de562..5871ee3c45 100644 --- a/playwright/utils/configs.ts +++ b/playwright/utils/configs.ts @@ -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 = { diff --git a/stubs/tx.ts b/stubs/tx.ts index dd26dc744c..1f7eb1c0a6 100644 --- a/stubs/tx.ts +++ b/stubs/tx.ts @@ -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 = []; diff --git a/stubs/zkEvmL2.ts b/stubs/zkEvmL2.ts new file mode 100644 index 0000000000..d68f1ac9d3 --- /dev/null +++ b/stubs/zkEvmL2.ts @@ -0,0 +1,24 @@ +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', +}; diff --git a/types/api/transaction.ts b/types/api/transaction.ts index 06c4056a82..2dec0a2f95 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -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 { diff --git a/types/api/zkEvmL2TxnBatches.ts b/types/api/zkEvmL2TxnBatches.ts new file mode 100644 index 0000000000..3a82b375ae --- /dev/null +++ b/types/api/zkEvmL2TxnBatches.ts @@ -0,0 +1,40 @@ +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; + 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; + verify_tx_hash: string; +} + +export type ZkEvmL2TxnBatchTxs = { + items: Array; + // 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 }; diff --git a/ui/address/internals/AddressIntTxsListItem.tsx b/ui/address/internals/AddressIntTxsListItem.tsx index 07bd1d39c0..98284c78e5 100644 --- a/ui/address/internals/AddressIntTxsListItem.tsx +++ b/ui/address/internals/AddressIntTxsListItem.tsx @@ -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 }; diff --git a/ui/address/internals/AddressIntTxsTableItem.tsx b/ui/address/internals/AddressIntTxsTableItem.tsx index 541b1a3e62..d07f3b931c 100644 --- a/ui/address/internals/AddressIntTxsTableItem.tsx +++ b/ui/address/internals/AddressIntTxsTableItem.tsx @@ -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 } diff --git a/ui/block/BlockDetails.tsx b/ui/block/BlockDetails.tsx index f37115e252..fb8b969466 100644 --- a/ui/block/BlockDetails.tsx +++ b/ui/block/BlockDetails.tsx @@ -38,6 +38,8 @@ interface Props { query: UseQueryResult; } +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 ( { ) } { { /* api doesn't return the block processing time yet */ } { /* { dayjs.duration(block.minedIn, 'second').humanize(true) } */ } - { !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 && ( { const totalReward = getBlockTotalReward(data); const burntFees = BigNumber(data.burnt_fees || 0); @@ -89,7 +91,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ) } - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && ( + { !isRollup && !config.UI.views.block.hiddenFields?.total_reward && ( Reward { config.chain.currency.symbol } @@ -97,7 +99,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ) } - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && ( + { !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && ( Burnt fees diff --git a/ui/blocks/BlocksTable.tsx b/ui/blocks/BlocksTable.tsx index 65c49cd16f..7540aff714 100644 --- a/ui/blocks/BlocksTable.tsx +++ b/ui/blocks/BlocksTable.tsx @@ -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 ( @@ -43,9 +45,9 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && + { !isRollup && !config.UI.views.block.hiddenFields?.total_reward && } - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && + { !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && } diff --git a/ui/blocks/BlocksTableItem.tsx b/ui/blocks/BlocksTableItem.tsx index 4676d82155..17c0710bbc 100644 --- a/ui/blocks/BlocksTableItem.tsx +++ b/ui/blocks/BlocksTableItem.tsx @@ -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) => { ) : data.tx_count } - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && ( + { !isRollup && !config.UI.views.block.hiddenFields?.total_reward && ( - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && ( + { !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && ( + + + + + + + + ); +}; + +export default TxnBatchesTableItem;
{ capitalize(getNetworkValidatorTitle()) } Txn Gas usedReward { config.chain.currency.symbol }Burnt fees { config.chain.currency.symbol }
{ BigNumber(data.gas_used || 0).toFormat() } @@ -109,7 +111,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { { totalReward.toFixed(8) } diff --git a/ui/home/LatestBlocks.pw.tsx b/ui/home/LatestBlocks.pw.tsx index 0e397533ea..8450ba1aa0 100644 --- a/ui/home/LatestBlocks.pw.tsx +++ b/ui/home/LatestBlocks.pw.tsx @@ -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 }) => { diff --git a/ui/home/LatestBlocks.tsx b/ui/home/LatestBlocks.tsx index f37799a68f..301963a6e2 100644 --- a/ui/home/LatestBlocks.tsx +++ b/ui/home/LatestBlocks.tsx @@ -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; diff --git a/ui/home/LatestBlocksItem.tsx b/ui/home/LatestBlocksItem.tsx index d59856105e..4dde7218db 100644 --- a/ui/home/LatestBlocksItem.tsx +++ b/ui/home/LatestBlocksItem.tsx @@ -59,14 +59,14 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => { Txn { block.tx_count } - { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && ( + { !config.features.optimisticRollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && ( <> Reward { totalReward.dp(10).toFixed() } ) } - { !config.features.rollup.isEnabled && ( + { !config.features.optimisticRollup.isEnabled && ( <> { getNetworkValidatorTitle() } { diff --git a/ui/home/LatestDepositsItem.tsx b/ui/home/LatestDepositsItem.tsx index 4ef9e6d349..c6f8fdc7b8 100644 --- a/ui/home/LatestDepositsItem.tsx +++ b/ui/home/LatestDepositsItem.tsx @@ -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; diff --git a/ui/home/LatestTxsItem.tsx b/ui/home/LatestTxsItem.tsx index 13b0b2ae16..9d04d7ea25 100644 --- a/ui/home/LatestTxsItem.tsx +++ b/ui/home/LatestTxsItem.tsx @@ -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'; diff --git a/ui/home/LatestTxsItemMobile.tsx b/ui/home/LatestTxsItemMobile.tsx index 0e375b4e4d..b6ced44217 100644 --- a/ui/home/LatestTxsItemMobile.tsx +++ b/ui/home/LatestTxsItemMobile.tsx @@ -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'; diff --git a/ui/home/LatestZkEvmL2Batches.pw.tsx b/ui/home/LatestZkEvmL2Batches.pw.tsx new file mode 100644 index 0000000000..5a470ac854 --- /dev/null +++ b/ui/home/LatestZkEvmL2Batches.pw.tsx @@ -0,0 +1,32 @@ +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( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/home/LatestZkEvmL2Batches.tsx b/ui/home/LatestZkEvmL2Batches.tsx new file mode 100644 index 0000000000..e6a24214b7 --- /dev/null +++ b/ui/home/LatestZkEvmL2Batches.tsx @@ -0,0 +1,90 @@ +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 } | 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 = No data. Please reload page.; + } + + if (data) { + const dataToShow = data.items.slice(0, batchesMaxCount); + + content = ( + <> + + + { dataToShow.map(((batch, index) => ( + + ))) } + + + + View all batches + + + ); + } + + return ( + + Latest batches + { content } + + ); +}; + +export default LatestZkEvmL2Batches; diff --git a/ui/home/LatestZkevmL2BatchItem.tsx b/ui/home/LatestZkevmL2BatchItem.tsx new file mode 100644 index 0000000000..bad789425e --- /dev/null +++ b/ui/home/LatestZkevmL2BatchItem.tsx @@ -0,0 +1,74 @@ +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 ( + + + + + + + + Txn + + + { batch.tx_count } + + + + + + + ); +}; + +export default LatestZkevmL2BatchItem; diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx index c7b317e538..b3fecac9f4 100644 --- a/ui/home/Stats.tsx +++ b/ui/home/Stats.tsx @@ -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,13 +56,23 @@ const Stats = () => { content = ( <> - + { config.features.zkEvmRollup.isEnabled ? ( + + ) : ( + + ) } { hasAvgBlockTime && ( { const hasAccount = useHasAccount(); - if (config.features.rollup.isEnabled || hasAccount) { + if (config.features.optimisticRollup.isEnabled || hasAccount) { const tabs = [ { id: 'txn', title: 'Latest txn', component: }, - config.features.rollup.isEnabled && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: }, + config.features.optimisticRollup.isEnabled && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: }, hasAccount && { id: 'watchlist', title: 'Watch list', component: }, ].filter(Boolean); return ( diff --git a/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_dark-color-mode_default-view-mobile-dark-mode-1.png b/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_dark-color-mode_default-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..fcf75937b4 Binary files /dev/null and b/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_dark-color-mode_default-view-mobile-dark-mode-1.png differ diff --git a/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_default_default-view-mobile-dark-mode-1.png b/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_default_default-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..1493485338 Binary files /dev/null and b/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_default_default-view-mobile-dark-mode-1.png differ diff --git a/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_mobile_default-view-mobile-dark-mode-1.png b/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_mobile_default-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..db7592d367 Binary files /dev/null and b/ui/home/__screenshots__/LatestZkEvmL2Batches.pw.tsx_mobile_default-view-mobile-dark-mode-1.png differ diff --git a/ui/l2Deposits/DepositsListItem.tsx b/ui/l2Deposits/DepositsListItem.tsx index e57839ecb8..7e4caefc57 100644 --- a/ui/l2Deposits/DepositsListItem.tsx +++ b/ui/l2Deposits/DepositsListItem.tsx @@ -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 }; diff --git a/ui/l2Deposits/DepositsTableItem.tsx b/ui/l2Deposits/DepositsTableItem.tsx index 32bd7eeb2b..99ba9ad465 100644 --- a/ui/l2Deposits/DepositsTableItem.tsx +++ b/ui/l2Deposits/DepositsTableItem.tsx @@ -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 }; diff --git a/ui/l2OutputRoots/OutputRootsListItem.tsx b/ui/l2OutputRoots/OutputRootsListItem.tsx index c39996c6f1..4a4a9337d0 100644 --- a/ui/l2OutputRoots/OutputRootsListItem.tsx +++ b/ui/l2OutputRoots/OutputRootsListItem.tsx @@ -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 }; diff --git a/ui/l2OutputRoots/OutputRootsTableItem.tsx b/ui/l2OutputRoots/OutputRootsTableItem.tsx index bbc011fdb8..e64b613a82 100644 --- a/ui/l2OutputRoots/OutputRootsTableItem.tsx +++ b/ui/l2OutputRoots/OutputRootsTableItem.tsx @@ -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 }; diff --git a/ui/l2TxnBatches/TxnBatchesListItem.tsx b/ui/l2TxnBatches/TxnBatchesListItem.tsx index 35e2a0426a..3efba7b03e 100644 --- a/ui/l2TxnBatches/TxnBatchesListItem.tsx +++ b/ui/l2TxnBatches/TxnBatchesListItem.tsx @@ -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 }; diff --git a/ui/l2TxnBatches/TxnBatchesTableItem.tsx b/ui/l2TxnBatches/TxnBatchesTableItem.tsx index 0dc151d449..8409b8e736 100644 --- a/ui/l2TxnBatches/TxnBatchesTableItem.tsx +++ b/ui/l2TxnBatches/TxnBatchesTableItem.tsx @@ -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 }; diff --git a/ui/l2Withdrawals/WithdrawalsListItem.tsx b/ui/l2Withdrawals/WithdrawalsListItem.tsx index 7076255f58..585b3f7688 100644 --- a/ui/l2Withdrawals/WithdrawalsListItem.tsx +++ b/ui/l2Withdrawals/WithdrawalsListItem.tsx @@ -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 }; diff --git a/ui/l2Withdrawals/WithdrawalsTableItem.tsx b/ui/l2Withdrawals/WithdrawalsTableItem.tsx index 9f4bc8fc25..6df6254116 100644 --- a/ui/l2Withdrawals/WithdrawalsTableItem.tsx +++ b/ui/l2Withdrawals/WithdrawalsTableItem.tsx @@ -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 }; diff --git a/ui/pages/Home.tsx b/ui/pages/Home.tsx index dd5424b7f6..dfd6c958af 100644 --- a/ui/pages/Home.tsx +++ b/ui/pages/Home.tsx @@ -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 = () => { - + { config.features.zkEvmRollup.isEnabled ? : } diff --git a/ui/pages/L2Deposits.pw.tsx b/ui/pages/L2Deposits.pw.tsx index e915c9fa3d..94c62178b8 100644 --- a/ui/pages/L2Deposits.pw.tsx +++ b/ui/pages/L2Deposits.pw.tsx @@ -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 }) => { diff --git a/ui/pages/L2OutputRoots.pw.tsx b/ui/pages/L2OutputRoots.pw.tsx index 34136616a1..d4f85eec48 100644 --- a/ui/pages/L2OutputRoots.pw.tsx +++ b/ui/pages/L2OutputRoots.pw.tsx @@ -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'); diff --git a/ui/pages/L2TxnBatches.pw.tsx b/ui/pages/L2TxnBatches.pw.tsx index 6e199d1e07..4216bb0c3b 100644 --- a/ui/pages/L2TxnBatches.pw.tsx +++ b/ui/pages/L2TxnBatches.pw.tsx @@ -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'); diff --git a/ui/pages/L2Withdrawals.pw.tsx b/ui/pages/L2Withdrawals.pw.tsx index aceccdc935..5ea8399a3f 100644 --- a/ui/pages/L2Withdrawals.pw.tsx +++ b/ui/pages/L2Withdrawals.pw.tsx @@ -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'); diff --git a/ui/pages/ZkEvmL2TxnBatch.pw.tsx b/ui/pages/ZkEvmL2TxnBatch.pw.tsx new file mode 100644 index 0000000000..852ecc3afb --- /dev/null +++ b/ui/pages/ZkEvmL2TxnBatch.pw.tsx @@ -0,0 +1,70 @@ +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( + + + , + { 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( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/ZkEvmL2TxnBatch.tsx b/ui/pages/ZkEvmL2TxnBatch.tsx new file mode 100644 index 0000000000..e2cb9811ff --- /dev/null +++ b/ui/pages/ZkEvmL2TxnBatch.tsx @@ -0,0 +1,86 @@ +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 = React.useMemo(() => ([ + { id: 'index', title: 'Details', component: }, + { id: 'txs', title: 'Transactions', component: }, + ].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 ( + <> + + + { batchQuery.isPlaceholderData ? : ( + + ) } + + ); +}; + +export default ZkEvmL2TxnBatch; diff --git a/ui/pages/ZkEvmL2TxnBatches.pw.tsx b/ui/pages/ZkEvmL2TxnBatches.pw.tsx new file mode 100644 index 0000000000..8c403a4620 --- /dev/null +++ b/ui/pages/ZkEvmL2TxnBatches.pw.tsx @@ -0,0 +1,43 @@ +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( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/ZkEvmL2TxnBatches.tsx b/ui/pages/ZkEvmL2TxnBatches.tsx new file mode 100644 index 0000000000..562be180c9 --- /dev/null +++ b/ui/pages/ZkEvmL2TxnBatches.tsx @@ -0,0 +1,83 @@ +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 ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + ) : null; + + const text = (() => { + if (countersQuery.isError || isError || !data?.items.length) { + return null; + } + + return ( + + Tx batch + #{ data.items[0].number } to + #{ data.items[data.items.length - 1].number } + (total of { countersQuery.data?.toLocaleString() } batches) + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ZkEvmL2TxnBatches; diff --git a/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png index 61bb5ddbb9..1d1532ab8c 100644 Binary files a/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..c59f29f714 Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..b0fd12db39 Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..99c8067c46 Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..8052eb9ef9 Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/shared/DetailsInfoItem.tsx b/ui/shared/DetailsInfoItem.tsx index 18c64f6c2e..1aa81cae90 100644 --- a/ui/shared/DetailsInfoItem.tsx +++ b/ui/shared/DetailsInfoItem.tsx @@ -6,7 +6,7 @@ import Hint from 'ui/shared/Hint'; interface Props extends Omit, '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 <> - + { hint && } { title } diff --git a/ui/shared/LinkExternal.tsx b/ui/shared/LinkExternal.tsx index 2ffd5f4d23..64401dab4a 100644 --- a/ui/shared/LinkExternal.tsx +++ b/ui/shared/LinkExternal.tsx @@ -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', }; diff --git a/ui/shared/entities/address/AddressEntityL1.tsx b/ui/shared/entities/address/AddressEntityL1.tsx index 877721614c..5df57f35d7 100644 --- a/ui/shared/entities/address/AddressEntityL1.tsx +++ b/ui/shared/entities/address/AddressEntityL1.tsx @@ -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) { diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png index eadd0597c5..5d824ab018 100644 Binary files a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png differ diff --git a/ui/shared/entities/block/BlockEntityL1.tsx b/ui/shared/entities/block/BlockEntityL1.tsx index 3275f62f12..6816ac8e8a 100644 --- a/ui/shared/entities/block/BlockEntityL1.tsx +++ b/ui/shared/entities/block/BlockEntityL1.tsx @@ -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' ]); diff --git a/ui/shared/entities/block/BlockEntityL2.tsx b/ui/shared/entities/block/BlockEntityL2.tsx index 8cbdf811c6..aa11730764 100644 --- a/ui/shared/entities/block/BlockEntityL2.tsx +++ b/ui/shared/entities/block/BlockEntityL2.tsx @@ -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' ]); diff --git a/ui/shared/entities/block/ZkEvmBatchEntityL2.tsx b/ui/shared/entities/block/ZkEvmBatchEntityL2.tsx new file mode 100644 index 0000000000..5ad926c1da --- /dev/null +++ b/ui/shared/entities/block/ZkEvmBatchEntityL2.tsx @@ -0,0 +1,35 @@ +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 ( + + + + + + + ); +}; + +export default chakra(ZkEvmBatchEntityL2); diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png b/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png index 75c402e818..af7a2cbe1a 100644 Binary files a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png and b/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png differ diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png b/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png index 089ee7e92b..40b249a491 100644 Binary files a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png and b/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png differ diff --git a/ui/shared/entities/tx/TxEntityL1.tsx b/ui/shared/entities/tx/TxEntityL1.tsx index 90f39afaa7..3b72f1894c 100644 --- a/ui/shared/entities/tx/TxEntityL1.tsx +++ b/ui/shared/entities/tx/TxEntityL1.tsx @@ -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' ]); diff --git a/ui/shared/entities/tx/__screenshots__/TxEntity.pw.tsx_default_external-link-1.png b/ui/shared/entities/tx/__screenshots__/TxEntity.pw.tsx_default_external-link-1.png index 7dca66fece..be6e12356d 100644 Binary files a/ui/shared/entities/tx/__screenshots__/TxEntity.pw.tsx_default_external-link-1.png and b/ui/shared/entities/tx/__screenshots__/TxEntity.pw.tsx_default_external-link-1.png differ diff --git a/ui/shared/statusTag/StatusTag.pw.tsx b/ui/shared/statusTag/StatusTag.pw.tsx new file mode 100644 index 0000000000..d289c54b5c --- /dev/null +++ b/ui/shared/statusTag/StatusTag.pw.tsx @@ -0,0 +1,37 @@ +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( + + + , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 75, height: 30 } }); +}); + +test('error status', async({ page, mount }) => { + await mount( + + + , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 75, height: 30 } }); + +}); + +test('pending status', async({ page, mount }) => { + await mount( + + + , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 75, height: 30 } }); +}); diff --git a/ui/shared/TxStatus.tsx b/ui/shared/statusTag/StatusTag.tsx similarity index 73% rename from ui/shared/TxStatus.tsx rename to ui/shared/statusTag/StatusTag.tsx index a77e0133cf..1075b1dfe7 100644 --- a/ui/shared/TxStatus.tsx +++ b/ui/shared/statusTag/StatusTag.tsx @@ -1,37 +1,34 @@ 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) => { - { label } + { text } ); }; -export default TxStatus; +export default StatusTag; diff --git a/ui/shared/statusTag/TxStatus.tsx b/ui/shared/statusTag/TxStatus.tsx new file mode 100644 index 0000000000..9fcfec58d4 --- /dev/null +++ b/ui/shared/statusTag/TxStatus.tsx @@ -0,0 +1,36 @@ +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 ; +}; + +export default TxStatus; diff --git a/ui/shared/statusTag/ZkEvmL2TxnBatchStatus.tsx b/ui/shared/statusTag/ZkEvmL2TxnBatchStatus.tsx new file mode 100644 index 0000000000..f7601bde46 --- /dev/null +++ b/ui/shared/statusTag/ZkEvmL2TxnBatchStatus.tsx @@ -0,0 +1,29 @@ +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 ; +}; + +export default ZkEvmL2TxnBatchStatus; diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png new file mode 100644 index 0000000000..456a05197e Binary files /dev/null and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png differ diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png new file mode 100644 index 0000000000..1f91068cef Binary files /dev/null and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png differ diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png new file mode 100644 index 0000000000..d5d9f0cec1 Binary files /dev/null and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png differ diff --git a/ui/shared/verificationSteps/VerificationStep.tsx b/ui/shared/verificationSteps/VerificationStep.tsx new file mode 100644 index 0000000000..efc9a294e8 --- /dev/null +++ b/ui/shared/verificationSteps/VerificationStep.tsx @@ -0,0 +1,26 @@ +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 ( + + + { step } + { !isLast && } + + ); +}; + +export default VerificationStep; diff --git a/ui/shared/verificationSteps/VerificationSteps.pw.tsx b/ui/shared/verificationSteps/VerificationSteps.pw.tsx new file mode 100644 index 0000000000..21e2f23518 --- /dev/null +++ b/ui/shared/verificationSteps/VerificationSteps.pw.tsx @@ -0,0 +1,33 @@ +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( + + + + + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('second status', async({ mount }) => { + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/verificationSteps/VerificationSteps.tsx b/ui/shared/verificationSteps/VerificationSteps.tsx new file mode 100644 index 0000000000..a07ec743c8 --- /dev/null +++ b/ui/shared/verificationSteps/VerificationSteps.tsx @@ -0,0 +1,30 @@ +import { Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import VerificationStep from './VerificationStep'; + +export interface Props { + step: T; + steps: Array; + isLoading?: boolean; +} + +const VerificationSteps = ({ step, steps, isLoading }: Props) => { + const currentStepIndex = steps.indexOf(step); + + return ( + + { steps.map((step, index) => ( + + )) } + + ); +}; + +export default VerificationSteps; diff --git a/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_dark-color-mode_first-step-mobile-dark-mode-1.png b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_dark-color-mode_first-step-mobile-dark-mode-1.png new file mode 100644 index 0000000000..1b723b918c Binary files /dev/null and b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_dark-color-mode_first-step-mobile-dark-mode-1.png differ diff --git a/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_default_first-step-mobile-dark-mode-1.png b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_default_first-step-mobile-dark-mode-1.png new file mode 100644 index 0000000000..ec5715c72c Binary files /dev/null and b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_default_first-step-mobile-dark-mode-1.png differ diff --git a/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_default_second-status-1.png b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_default_second-status-1.png new file mode 100644 index 0000000000..bed6995133 Binary files /dev/null and b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_default_second-status-1.png differ diff --git a/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_mobile_first-step-mobile-dark-mode-1.png b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_mobile_first-step-mobile-dark-mode-1.png new file mode 100644 index 0000000000..cf24c70de8 Binary files /dev/null and b/ui/shared/verificationSteps/__screenshots__/VerificationSteps.pw.tsx_mobile_first-step-mobile-dark-mode-1.png differ diff --git a/ui/token/TokenVerifiedInfo.tsx b/ui/token/TokenVerifiedInfo.tsx index 13765829dc..136929caf9 100644 --- a/ui/token/TokenVerifiedInfo.tsx +++ b/ui/token/TokenVerifiedInfo.tsx @@ -40,7 +40,7 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => { try { const url = new URL(data.projectWebsite); return ( - + { url.host } ); diff --git a/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_default_base-view-mobile-1.png b/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_default_base-view-mobile-1.png index 10b3d019cc..e8099dc08a 100644 Binary files a/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_default_base-view-mobile-1.png and b/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_mobile_base-view-mobile-1.png b/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_mobile_base-view-mobile-1.png index fe13eeb41b..a14681595e 100644 Binary files a/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_mobile_base-view-mobile-1.png and b/ui/tokenInstance/__screenshots__/TokenInstanceMetadata.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/tokenInstance/details/TokenInstanceMetadataInfo.tsx b/ui/tokenInstance/details/TokenInstanceMetadataInfo.tsx index 17720a0aaa..85dd53856c 100644 --- a/ui/tokenInstance/details/TokenInstanceMetadataInfo.tsx +++ b/ui/tokenInstance/details/TokenInstanceMetadataInfo.tsx @@ -33,6 +33,8 @@ const Item = ({ data, isLoading }: ItemProps) => { w="100%" overflow="hidden" href={ data.value } + fontSize="sm" + lineHeight={ 5 } > diff --git a/ui/tx/TxDetails.pw.tsx b/ui/tx/TxDetails.pw.tsx index 8b2a19aa2b..2c59d2a55c 100644 --- a/ui/tx/TxDetails.pw.tsx +++ b/ui/tx/TxDetails.pw.tsx @@ -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 }) => { diff --git a/ui/tx/TxDetails.tsx b/ui/tx/TxDetails.tsx index 06e979f2a6..fbbbd29da2 100644 --- a/ui/tx/TxDetails.tsx +++ b/ui/tx/TxDetails.tsx @@ -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 = () => { @@ -152,6 +156,15 @@ const TxDetails = () => { ) } + { data.zkevm_status && ( + + + + ) } { data.revert_reason && ( { ) } + { data.zkevm_batch_number && ( + + + + ) } { data.timestamp && ( { + { data.zkevm_sequence_hash && ( + + + + + + + ) } + { data.zkevm_verify_hash && ( + + + + + + + + + ) } + + { (data.zkevm_batch_number || data.zkevm_verify_hash) && } + { !config.UI.views.tx.hiddenFields?.value && ( { ) } ) } - { 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 && ( { /> ) } - { config.features.rollup.isEnabled && ( + { config.features.optimisticRollup.isEnabled && ( <> { data.l1_gas_used && ( 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; diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index db2e82f199..fb81ae48b3 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -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; diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index 329532a06b..4d1b72eb69 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -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'; diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index b0b52fe13b..42443476ef 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -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'; diff --git a/ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails.tsx b/ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails.tsx new file mode 100644 index 0000000000..dd0f6fbcbf --- /dev/null +++ b/ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails.tsx @@ -0,0 +1,175 @@ +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; +} + +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 ; + } + + if (!data) { + return null; + } + + return ( + + + + { data.number } + + + + + + + + + + { dayjs(data.timestamp).fromNow() } + + + + { dayjs(data.timestamp).format('llll') } + + + + { data.verify_tx_hash ? ( + + ) : pending } + + + + + { data.transactions.length } transaction{ data.transactions.length === 1 ? '' : 's' } + + + + + + + + + + + + + + + + + + + + { data.sequence_tx_hash ? ( + + ) : Pending } + + + + + + + + + ); +}; + +export default ZkEvmL2TxnBatchDetails; diff --git a/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesListItem.tsx b/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesListItem.tsx new file mode 100644 index 0000000000..4b99a2e6c3 --- /dev/null +++ b/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesListItem.tsx @@ -0,0 +1,94 @@ +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 ( + + + Batch # + + + + + Status + + + + + Age + + { timeAgo } + + + Txn count + + + + { item.tx_count } + + + + + Verify Tx Has + + { item.verify_tx_hash ? ( + + ) : Pending } + + + Sequence hash + + { item.sequence_tx_hash ? ( + + ) : Pending } + + + + ); +}; + +export default ZkEvmTxnBatchesListItem; diff --git a/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTable.tsx b/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTable.tsx new file mode 100644 index 0000000000..fabbc0ae70 --- /dev/null +++ b/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTable.tsx @@ -0,0 +1,42 @@ +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; + top: number; + isLoading?: boolean; +} + +const TxnBatchesTable = ({ items, top, isLoading }: Props) => { + return ( + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
Batch #StatusAgeTxn countVerify Tx HasSequence hash
+ ); +}; + +export default TxnBatchesTable; diff --git a/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTableItem.tsx b/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTableItem.tsx new file mode 100644 index 0000000000..3485414e71 --- /dev/null +++ b/ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTableItem.tsx @@ -0,0 +1,81 @@ +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 ( +
+ + + + + + { timeAgo } + + + + + { item.tx_count } + + + + { item.verify_tx_hash ? ( + + ) : Pending } + + { item.sequence_tx_hash ? ( + + ) : Pending } +