Commit 9e3e337f authored by tom goriunov's avatar tom goriunov Committed by GitHub

OP rollup: Batch view and DA fields (#2210)

* tx batches view adjustments

* blocks and txs tabs of batch page

* batch details

* add batch info to block details view

* display batch blob data in EIP-4844 and calldata container

* display batch blob data in celestia container

* tests

* add optimism_celestia preset

* review fixes

* update screenshots
parent b5abddce
...@@ -20,6 +20,7 @@ on: ...@@ -20,6 +20,7 @@ on:
- eth_sepolia - eth_sepolia
- eth_goerli - eth_goerli
- optimism - optimism
- optimism_celestia
- optimism_sepolia - optimism_sepolia
- polygon - polygon
- rootstock - rootstock
......
...@@ -20,6 +20,7 @@ on: ...@@ -20,6 +20,7 @@ on:
- eth_sepolia - eth_sepolia
- eth_goerli - eth_goerli
- optimism - optimism
- optimism_celestia
- optimism_sepolia - optimism_sepolia
- polygon - polygon
- rootstock - rootstock
......
...@@ -368,6 +368,7 @@ ...@@ -368,6 +368,7 @@
"eth_goerli", "eth_goerli",
"eth_sepolia", "eth_sepolia",
"optimism", "optimism",
"optimism_celestia",
"optimism_sepolia", "optimism_sepolia",
"polygon", "polygon",
"rootstock_testnet", "rootstock_testnet",
......
...@@ -2,6 +2,8 @@ import type { Feature } from './types'; ...@@ -2,6 +2,8 @@ import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup'; import type { RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup'; import { ROLLUP_TYPES } from 'types/client/rollup';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const type = (() => { const type = (() => {
...@@ -21,7 +23,7 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s ...@@ -21,7 +23,7 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s
title, title,
isEnabled: true, isEnabled: true,
type, type,
L1BaseUrl, L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl, L2WithdrawalUrl,
}); });
} }
......
# Set of ENVs for OP Celestia Raspberry network explorer
# https://opcelestia-raspberry.gelatoscout.com
# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism_celestia"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'721628','width':'728','height':'90'}
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'721627','width':'300','height':'100'}
NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
NEXT_PUBLIC_AD_TEXT_PROVIDER=none
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=opcelestia-raspberry.gelatoscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/opcelestia-raspberry.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x0f5b54de81848d8d8baa02c69030037218a2b4df622d64a2a429e11721606656
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(255, 0, 0, 1)
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ID=123420111
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_NAME=OP Celestia Raspberry
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.opcelestia-raspberry.gelato.digital
NEXT_PUBLIC_NETWORK_SHORT_NAME=opcelestia-raspberry
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.gelato.network/bridge/opcelestia-raspberry
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_STATS_API_HOST=https://stats-opcelestia-raspberry.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=none
\ No newline at end of file
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="url(#a)"/>
<g clip-path="url(#b)" fill="#fff" fill-opacity=".95">
<path d="M15.763 12.826c.119-.243.178-.365.158-.462a.292.292 0 0 0-.148-.199c-.088-.047-.244-.02-.554.033a6.409 6.409 0 0 1-5.632-1.786 6.409 6.409 0 0 1-1.785-5.631c.053-.31.08-.466.033-.554a.292.292 0 0 0-.199-.149c-.098-.02-.219.04-.462.159a6.417 6.417 0 1 0 8.589 8.589Z"/>
<path d="M15.9 10.817c.152-.054.229-.082.31-.152a.686.686 0 0 0 .163-.228c.04-.1.04-.183.043-.35a6.398 6.398 0 0 0-1.879-4.624 6.398 6.398 0 0 0-4.624-1.88c-.167.003-.25.004-.35.044a.685.685 0 0 0-.229.163c-.07.081-.097.158-.151.31a5.25 5.25 0 0 0 6.717 6.717Z"/>
</g>
<defs>
<linearGradient id="a" x1="17.5" y1="2" x2="0" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#196E41"/>
<stop offset="1" stop-color="#092E1B"/>
</linearGradient>
<clipPath id="b">
<rect x="3" y="3" width="14" height="14" rx="7" fill="#fff"/>
</clipPath>
</defs>
</svg>
...@@ -85,6 +85,9 @@ import type { ...@@ -85,6 +85,9 @@ import type {
OptimisticL2TxnBatchesResponse, OptimisticL2TxnBatchesResponse,
OptimisticL2WithdrawalsResponse, OptimisticL2WithdrawalsResponse,
OptimisticL2DisputeGamesResponse, OptimisticL2DisputeGamesResponse,
OptimismL2TxnBatch,
OptimismL2BatchTxs,
OptimismL2BatchBlocks,
} from 'types/api/optimisticL2'; } from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
...@@ -678,12 +681,29 @@ export const RESOURCES = { ...@@ -678,12 +681,29 @@ export const RESOURCES = {
}, },
optimistic_l2_txn_batches: { optimistic_l2_txn_batches: {
path: '/api/v2/optimism/txn-batches', path: '/api/v2/optimism/batches',
filterFields: [], filterFields: [],
}, },
optimistic_l2_txn_batches_count: { optimistic_l2_txn_batches_count: {
path: '/api/v2/optimism/txn-batches/count', path: '/api/v2/optimism/batches/count',
},
optimistic_l2_txn_batch: {
path: '/api/v2/optimism/batches/:number',
pathParams: [ 'number' as const ],
},
optimistic_l2_txn_batch_txs: {
path: '/api/v2/transactions/optimism-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
optimistic_l2_txn_batch_blocks: {
path: '/api/v2/blocks/optimism-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
}, },
optimistic_l2_dispute_games: { optimistic_l2_dispute_games: {
...@@ -967,7 +987,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -967,7 +987,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' | 'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
'optimistic_l2_dispute_games' | 'optimistic_l2_dispute_games' | 'optimistic_l2_txn_batch_txs' | 'optimistic_l2_txn_batch_blocks' |
'mud_worlds'| 'address_mud_tables' | 'address_mud_records' | 'mud_worlds'| 'address_mud_tables' | 'address_mud_records' |
'shibarium_deposits' | 'shibarium_withdrawals' | 'shibarium_deposits' | 'shibarium_withdrawals' |
'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' | 'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' |
...@@ -1072,11 +1092,14 @@ Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse : ...@@ -1072,11 +1092,14 @@ Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse : Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse : Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse : Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'optimistic_l2_txn_batch' ? OptimismL2TxnBatch :
Q extends 'optimistic_l2_txn_batch_txs' ? OptimismL2BatchTxs :
Q extends 'optimistic_l2_txn_batch_blocks' ? OptimismL2BatchBlocks :
Q extends 'optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse : Q extends 'optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse :
Q extends 'optimistic_l2_output_roots_count' ? number : Q extends 'optimistic_l2_output_roots_count' ? number :
Q extends 'optimistic_l2_withdrawals_count' ? number : Q extends 'optimistic_l2_withdrawals_count' ? number :
Q extends 'optimistic_l2_deposits_count' ? number : Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'optimistic_l2_dispute_games_count' ? number : Q extends 'optimistic_l2_dispute_games_count' ? number :
never; never;
// !!! IMPORTANT !!! // !!! IMPORTANT !!!
......
export const txnBatchesData = { import type {
OptimismL2TxnBatchTypeCallData,
OptimismL2TxnBatchTypeCelestia,
OptimismL2TxnBatchTypeEip4844,
OptimisticL2TxnBatchesResponse,
} from 'types/api/optimisticL2';
export const txnBatchesData: OptimisticL2TxnBatchesResponse = {
items: [ items: [
{ {
batch_data_container: 'in_blob4844',
internal_id: 260998,
l1_timestamp: '2022-11-10T11:29:11.000000Z',
l1_tx_hashes: [ l1_tx_hashes: [
'0x5bc94d02b65743dfaa9e10a2d6e175aff2a05cce2128c8eaf848bd84ab9325c5', '0x9553351f6bd1577f4e782738c087be08697fb11f3b91745138d71ba166d62c3b',
'0x92a51bc623111dbb91f243e3452e60fab6f090710357f9d9b75ac8a0f67dfd9d',
], ],
l1_timestamp: '2023-02-24T10:16:12.000000Z', l2_block_end: 124882074,
l2_block_number: 5902836, l2_block_start: 124881833,
tx_count: 0, tx_count: 4011,
}, },
{ {
batch_data_container: 'in_calldata',
internal_id: 260997,
l1_timestamp: '2022-11-03T11:20:59.000000Z',
l1_tx_hashes: [ l1_tx_hashes: [
'0xc45f846ee28ce9ba116ce2d378d3dd00b55d324b833b3ecd4241c919c572c4aa', '0x80f5fba70d5685bc2b70df836942e892b24afa7bba289a2fac0ca8f4d554cc72',
], ],
l1_timestamp: '2023-02-24T10:16:00.000000Z', l2_block_end: 124881832,
l2_block_number: 5902835, l2_block_start: 124881613,
tx_count: 0, tx_count: 4206,
}, },
{ {
internal_id: 260996,
l1_timestamp: '2024-09-03T11:14:23.000000Z',
l1_tx_hashes: [ l1_tx_hashes: [
'0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8', '0x39f4c46cae57bae936acb9159e367794f41f021ed3788adb80ad93830edb5f22',
], ],
l1_timestamp: '2023-02-24T10:16:00.000000Z', l2_block_end: 124881612,
l2_block_number: 5902834, l2_block_start: 124881380,
tx_count: 0, tx_count: 4490,
}, },
], ],
next_page_params: { next_page_params: {
block_number: 5902834, id: 5902834,
items_count: 50, items_count: 50,
}, },
}; };
export const txnBatchTypeCallData: OptimismL2TxnBatchTypeCallData = {
batch_data_container: 'in_calldata',
internal_id: 309123,
l1_timestamp: '2022-08-10T10:30:24.000000Z',
l1_tx_hashes: [
'0x478c45f182631ae6f7249d40f31fdac36f41d88caa2e373fba35340a7345ca67',
],
l2_block_end: 10146784,
l2_block_start: 10145379,
tx_count: 1608,
};
export const txnBatchTypeCelestia: OptimismL2TxnBatchTypeCelestia = {
batch_data_container: 'in_celestia',
blobs: [
{
commitment: '0x39c18c21c6b127d58809b8d3b5931472421f9b51532959442f53038f10b78f2a',
height: 2584868,
l1_timestamp: '2024-08-28T16:51:12.000000Z',
l1_transaction_hash: '0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24',
namespace: '0x00000000000000000000000000000000000000000008e5f679bf7116cb',
},
],
internal_id: 309667,
l1_timestamp: '2022-08-28T16:51:12.000000Z',
l1_tx_hashes: [
'0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24',
],
l2_block_end: 10935879,
l2_block_start: 10934514,
tx_count: 1574,
};
export const txnBatchTypeEip4844: OptimismL2TxnBatchTypeEip4844 = {
batch_data_container: 'in_blob4844',
blobs: [
{
hash: '0x012a4f0c6db6bce9d3d357b2bf847764320bcb0107ab318f3a532f637bc60dfe',
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
},
{
hash: '0x01d1097cce23229931afbc2fd1cf0d707da26df7b39cef1c542276ae718de4f6',
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
},
],
internal_id: 2538459,
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_tx_hashes: [
'0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
],
l2_block_end: 16291502,
l2_block_start: 16291373,
tx_count: 704,
};
...@@ -112,7 +112,7 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => { ...@@ -112,7 +112,7 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum' ]; const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic' ];
export const batch: GetServerSideProps<Props> = async(context) => { export const batch: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) { if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) {
return { return {
......
...@@ -17,6 +17,8 @@ const Batch = dynamic(() => { ...@@ -17,6 +17,8 @@ const Batch = dynamic(() => {
switch (rollupFeature.type) { switch (rollupFeature.type) {
case 'arbitrum': case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatch'); return import('ui/pages/ArbitrumL2TxnBatch');
case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatch');
case 'zkEvm': case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatch'); return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync': case 'zkSync':
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
| "block_slim" | "block_slim"
| "block" | "block"
| "brands/blockscout" | "brands/blockscout"
| "brands/celenium"
| "brands/safe" | "brands/safe"
| "brands/solidity_scan" | "brands/solidity_scan"
| "burger" | "burger"
......
import type { import type {
OptimismL2TxnBatch,
OptimisticL2DepositsItem, OptimisticL2DepositsItem,
OptimisticL2DisputeGamesItem, OptimisticL2DisputeGamesItem,
OptimisticL2OutputRootsItem, OptimisticL2OutputRootsItem,
...@@ -30,14 +31,29 @@ export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = { ...@@ -30,14 +31,29 @@ export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = {
}; };
export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = { export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = {
internal_id: 260991,
batch_data_container: 'in_blob4844',
l1_timestamp: '2023-06-01T14:46:48.000000Z', l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [ l1_tx_hashes: [
TX_HASH, TX_HASH,
], ],
l2_block_number: 5218590, l2_block_start: 5218590,
l2_block_end: 5218777,
tx_count: 9, tx_count: 9,
}; };
export const L2_TXN_BATCH: OptimismL2TxnBatch = {
...L2_TXN_BATCHES_ITEM,
batch_data_container: 'in_blob4844',
blobs: [
{
hash: '0x01fb41e1ae9f827e13abb0ee94be2ee574a23ac31426cea630ddd18af854bc85',
l1_timestamp: '2024-09-03T13:26:23.000000Z',
l1_transaction_hash: '0xd25ee571f1701690615099b208a9431d8611d0130dc342bead6d9edc291f04b9',
},
],
};
export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = { export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = {
l1_block_number: 9103684, l1_block_number: 9103684,
l1_timestamp: '2023-06-01T15:26:12.000000Z', l1_timestamp: '2023-06-01T15:26:12.000000Z',
......
...@@ -11,6 +11,7 @@ const PRESETS = { ...@@ -11,6 +11,7 @@ const PRESETS = {
eth_sepolia: 'https://eth-sepolia.blockscout.com', eth_sepolia: 'https://eth-sepolia.blockscout.com',
gnosis: 'https://gnosis.blockscout.com', gnosis: 'https://gnosis.blockscout.com',
optimism: 'https://optimism.blockscout.com', optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
optimism_sepolia: 'https://optimism-sepolia.blockscout.com', optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
polygon: 'https://polygon.blockscout.com', polygon: 'https://polygon.blockscout.com',
rootstock_testnet: 'https://rootstock-testnet.blockscout.com', rootstock_testnet: 'https://rootstock-testnet.blockscout.com',
......
...@@ -3,6 +3,7 @@ import type { Reward } from 'types/api/reward'; ...@@ -3,6 +3,7 @@ import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2'; import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2';
import type { TokenInfo } from './token'; import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
import type { ZkSyncBatchesItem } from './zkSyncL2'; import type { ZkSyncBatchesItem } from './zkSyncL2';
...@@ -59,6 +60,7 @@ export interface Block { ...@@ -59,6 +60,7 @@ export interface Block {
'batch_number': number | null; 'batch_number': number | null;
}; };
arbitrum?: ArbitrumBlockData; arbitrum?: ArbitrumBlockData;
optimism?: OptimismBlockData;
// CELO FIELDS // CELO FIELDS
celo?: { celo?: {
epoch_number: number; epoch_number: number;
...@@ -78,6 +80,14 @@ type ArbitrumBlockData = { ...@@ -78,6 +80,14 @@ type ArbitrumBlockData = {
'status': ArbitrumBatchStatus; 'status': ArbitrumBatchStatus;
} }
export interface OptimismBlockData {
batch_data_container: OptimisticL2BatchDataContainer;
internal_id: number;
blobs: Array<OptimisticL2BlobTypeEip4844> | Array<OptimisticL2BlobTypeCelestia> | null;
l1_timestamp: string;
l1_tx_hashes: Array<string>;
}
export interface BlocksResponse { export interface BlocksResponse {
items: Array<Block>; items: Array<Block>;
next_page_params: { next_page_params: {
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { Block } from './block';
import type { Transaction } from './transaction';
export type OptimisticL2DepositsItem = { export type OptimisticL2DepositsItem = {
l1_block_number: number; l1_block_number: number;
...@@ -35,21 +37,82 @@ export type OptimisticL2OutputRootsResponse = { ...@@ -35,21 +37,82 @@ export type OptimisticL2OutputRootsResponse = {
}; };
} }
export type OptimisticL2BatchDataContainer = 'in_blob4844' | 'in_celestia' | 'in_calldata';
export type OptimisticL2TxnBatchesItem = { export type OptimisticL2TxnBatchesItem = {
l1_tx_hashes: Array<string>; internal_id: number;
batch_data_container?: OptimisticL2BatchDataContainer;
l1_timestamp: string; l1_timestamp: string;
l2_block_number: number; l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number; tx_count: number;
} }
export type OptimisticL2TxnBatchesResponse = { export type OptimisticL2TxnBatchesResponse = {
items: Array<OptimisticL2TxnBatchesItem>; items: Array<OptimisticL2TxnBatchesItem>;
next_page_params: { next_page_params: {
block_number: number; id: number;
items_count: number; items_count: number;
}; };
} }
export interface OptimisticL2BlobTypeEip4844 {
hash: string;
l1_timestamp: string;
l1_transaction_hash: string;
}
export interface OptimisticL2BlobTypeCelestia {
commitment: string;
height: number;
l1_timestamp: string;
l1_transaction_hash: string;
namespace: string;
}
interface OptimismL2TxnBatchBase {
internal_id: number;
l1_timestamp: string;
l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number;
}
export interface OptimismL2TxnBatchTypeCallData extends OptimismL2TxnBatchBase {
batch_data_container: 'in_calldata';
}
export interface OptimismL2TxnBatchTypeEip4844 extends OptimismL2TxnBatchBase {
batch_data_container: 'in_blob4844';
blobs: Array<OptimisticL2BlobTypeEip4844> | null;
}
export interface OptimismL2TxnBatchTypeCelestia extends OptimismL2TxnBatchBase {
batch_data_container: 'in_celestia';
blobs: Array<OptimisticL2BlobTypeCelestia> | null;
}
export type OptimismL2TxnBatch = OptimismL2TxnBatchTypeCallData | OptimismL2TxnBatchTypeEip4844 | OptimismL2TxnBatchTypeCelestia;
export type OptimismL2BatchTxs = {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
} | null;
}
export type OptimismL2BatchBlocks = {
items: Array<Block>;
next_page_params: {
batch_number: number;
items_count: number;
} | null;
}
export type OptimisticL2WithdrawalsItem = { export type OptimisticL2WithdrawalsItem = {
'challenge_period_end': string | null; 'challenge_period_end': string | null;
'from': AddressParam | null; 'from': AddressParam | null;
......
...@@ -19,6 +19,7 @@ import getNetworkValidationActionText from 'lib/networks/getNetworkValidationAct ...@@ -19,6 +19,7 @@ import getNetworkValidationActionText from 'lib/networks/getNetworkValidationAct
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
...@@ -202,7 +203,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -202,7 +203,7 @@ const BlockDetails = ({ query }: Props) => {
hint="Batch number" hint="Batch number"
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
> >
Batch Batch
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
{ data.arbitrum.batch_number ? { data.arbitrum.batch_number ?
...@@ -212,6 +213,28 @@ const BlockDetails = ({ query }: Props) => { ...@@ -212,6 +213,28 @@ const BlockDetails = ({ query }: Props) => {
</> </>
) } ) }
{ rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.optimism && !config.UI.views.block.hiddenFields?.batch && (
<>
<DetailsInfoItem.Label
hint="Batch number"
isLoading={ isPlaceholderData }
>
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value columnGap={ 3 }>
{ data.optimism.internal_id ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.optimism.internal_id }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
{ data.optimism.batch_data_container && (
<OptimisticL2TxnBatchDA
container={ data.optimism.batch_data_container }
isLoading={ isPlaceholderData }
/>
) }
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItem.Label <DetailsInfoItem.Label
hint="Size of the block in bytes" hint="Size of the block in bytes"
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
......
...@@ -26,7 +26,7 @@ const TABS_HEIGHT = 88; ...@@ -26,7 +26,7 @@ const TABS_HEIGHT = 88;
interface Props { interface Props {
type?: BlockType; type?: BlockType;
query: QueryWithPagesResult<'blocks'>; query: QueryWithPagesResult<'blocks'> | QueryWithPagesResult<'optimistic_l2_txn_batch_blocks'>;
enableSocket?: boolean; enableSocket?: boolean;
top?: number; top?: number;
} }
......
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
import { L2_TXN_BATCH } from 'stubs/L2';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import BlocksContent from 'ui/blocks/BlocksContent';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import OptimisticL2TxnBatchDetails from 'ui/txnBatches/optimisticL2/OptimisticL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const TABS_HEIGHT = 80;
const OptimisticL2TxnBatch = () => {
const router = useRouter();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
const tab = getQueryParamString(router.query.tab);
const isMobile = useIsMobile();
const batchQuery = useApiQuery('optimistic_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: L2_TXN_BATCH,
},
});
const batchTxsQuery = useQueryWithPages({
resourceName: 'optimistic_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.internal_id && tab === 'txs'),
placeholderData: generateListStub<'optimistic_l2_txn_batch_txs'>(TX, 50, { next_page_params: {
block_number: 1338932,
index: 1,
items_count: 50,
} }),
},
});
const batchBlocksQuery = useQueryWithPages({
resourceName: 'optimistic_l2_txn_batch_blocks',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.internal_id && tab === 'blocks'),
placeholderData: generateListStub<'optimistic_l2_txn_batch_blocks'>(BLOCK, 50, { next_page_params: {
batch_number: 1338932,
items_count: 50,
} }),
},
});
throwOnAbsentParamError(number);
throwOnResourceLoadError(batchQuery);
let pagination;
if (tab === 'txs') {
pagination = batchTxsQuery.pagination;
}
if (tab === 'blocks') {
pagination = batchBlocksQuery.pagination;
}
const hasPagination = !isMobile && pagination?.isVisible;
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <OptimisticL2TxnBatchDetails query={ batchQuery }/> },
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
title: 'Blocks',
component: <BlocksContent type="block" query={ batchBlocksQuery } enableSocket={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
].filter(Boolean)), [ batchQuery, batchTxsQuery, batchBlocksQuery, hasPagination ]);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to tx batches list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `Batch #${ number }` }
backLink={ backLink }
/>
{ batchQuery.isPlaceholderData ?
<TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
</>
);
};
export default OptimisticL2TxnBatch;
...@@ -23,7 +23,7 @@ const OptimisticL2TxnBatches = () => { ...@@ -23,7 +23,7 @@ const OptimisticL2TxnBatches = () => {
{ {
next_page_params: { next_page_params: {
items_count: 50, items_count: 50,
block_number: 9045200, id: 9045200,
}, },
}, },
), ),
...@@ -41,7 +41,7 @@ const OptimisticL2TxnBatches = () => { ...@@ -41,7 +41,7 @@ const OptimisticL2TxnBatches = () => {
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => ( { data.items.map(((item, index) => (
<OptimisticL2TxnBatchesListItem <OptimisticL2TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') } key={ item.internal_id + (isPlaceholderData ? String(index) : '') }
item={ item } item={ item }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
...@@ -61,8 +61,8 @@ const OptimisticL2TxnBatches = () => { ...@@ -61,8 +61,8 @@ const OptimisticL2TxnBatches = () => {
return ( return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block) Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].internal_id } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].internal_id } </Text>
(total of { countersQuery.data?.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
</Skeleton> </Skeleton>
); );
......
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import type { ExcludeUndefined } from 'types/utils';
import Tag from 'ui/shared/chakra/Tag';
export interface Props {
container: ExcludeUndefined<OptimisticL2TxnBatchesItem['batch_data_container']>;
isLoading?: boolean;
}
const OptimisticL2TxnBatchDA = ({ container, isLoading }: Props) => {
const text = (() => {
switch (container) {
case 'in_blob4844':
return 'EIP-4844 blob';
case 'in_calldata':
return 'Calldata';
case 'in_celestia':
return 'Celestia blob';
}
})();
return (
<Tag colorScheme="yellow" isLoading={ isLoading }>
{ text }
</Tag>
);
};
export default React.memo(OptimisticL2TxnBatchDA);
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 * as BlobEntity from './BlobEntity';
const rollupFeature = config.features.rollup;
const BlobEntityL1 = (props: BlobEntity.EntityProps) => {
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const linkProps = _omit(props, [ 'className' ]);
if (!rollupFeature.isEnabled) {
return null;
}
return (
<BlobEntity.Container className={ props.className }>
<BlobEntity.Icon { ...partsProps }/>
<BlobEntity.Link
{ ...linkProps }
isExternal
href={ rollupFeature.L1BaseUrl + route({ pathname: '/blobs/[hash]', query: { hash: props.hash } }) }
>
<BlobEntity.Content { ...partsProps }/>
</BlobEntity.Link>
<BlobEntity.Copy { ...partsProps }/>
</BlobEntity.Container>
);
};
export default chakra(BlobEntityL1);
import { GridItem } from '@chakra-ui/react';
import React from 'react';
import dayjs from 'lib/date/dayjs';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
interface Props {
l1TxHashes: Array<string>;
l1Timestamp: string;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobCallData = ({ l1TxHashes, l1Timestamp, isLoading }: Props) => {
return (
<OptimisticL2TxnBatchBlobWrapper isLoading={ isLoading }>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal">
{ dayjs(l1Timestamp).fromNow() } | { dayjs(l1Timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash{ l1TxHashes.length > 1 ? 'es' : '' }</GridItem>
<GridItem overflow="hidden" display="flex" flexDir="column" rowGap={ 2 }>
{ l1TxHashes.map((hash) => <TxEntityL1 key={ hash } hash={ hash } noIcon noCopy={ false }/>) }
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
};
export default React.memo(OptimisticL2TxnBatchBlobCallData);
import { Flex, GridItem, Icon, VStack } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2BlobTypeCelestia } from 'types/api/optimisticL2';
// This icon doesn't work properly when it is in the sprite
// Probably because of the gradient
// eslint-disable-next-line no-restricted-imports
import celeniumIcon from 'icons/brands/celenium.svg';
import dayjs from 'lib/date/dayjs';
import hexToBase64 from 'lib/hexToBase64';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/links/LinkExternal';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
function getCeleniumUrl(blob: OptimisticL2BlobTypeCelestia) {
const url = new URL('https://mocha.celenium.io/blob');
url.searchParams.set('commitment', hexToBase64(blob.commitment));
url.searchParams.set('hash', hexToBase64(blob.namespace));
url.searchParams.set('height', String(blob.height));
return url.toString();
}
interface Props {
blobs: Array<OptimisticL2BlobTypeCelestia>;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobCelestia = ({ blobs, isLoading }: Props) => {
return (
<VStack rowGap={ 2 } w="100%">
{ blobs.map((blob) => {
return (
<OptimisticL2TxnBatchBlobWrapper key={ blob.commitment } isLoading={ isLoading } gridTemplateColumns="auto 1fr auto">
<GridItem fontWeight={ 600 }>Commitment</GridItem>
<GridItem overflow="hidden">
<Flex minW="0" w="calc(100% - 20px)">
<HashStringShortenDynamic hash={ blob.commitment }/>
<CopyToClipboard text={ blob.commitment }/>
</Flex>
</GridItem>
<GridItem display="flex" columnGap={ 2 }>
<Icon as={ celeniumIcon } boxSize={ 5 }/>
<LinkExternal href={ getCeleniumUrl(blob) }>Blob page</LinkExternal>
</GridItem>
<GridItem fontWeight={ 600 }>Hight</GridItem>
<GridItem colSpan={ 2 }>
{ blob.height }
</GridItem>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal" colSpan={ 2 }>
{ dayjs(blob.l1_timestamp).fromNow() } | { dayjs(blob.l1_timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash</GridItem>
<GridItem overflow="hidden" colSpan={ 2 }>
<TxEntityL1 hash={ blob.l1_transaction_hash } noIcon noCopy={ false }/>
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
}) }
</VStack>
);
};
export default React.memo(OptimisticL2TxnBatchBlobCelestia);
import { GridItem, VStack } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2BlobTypeEip4844 } from 'types/api/optimisticL2';
import dayjs from 'lib/date/dayjs';
import BlobEntityL1 from 'ui/shared/entities/blob/BlobEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
interface Props {
blobs: Array<OptimisticL2BlobTypeEip4844>;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobEip4844 = ({ blobs, isLoading }: Props) => {
return (
<VStack rowGap={ 2 } w="100%">
{ blobs.map((blob) => {
return (
<OptimisticL2TxnBatchBlobWrapper key={ blob.hash } isLoading={ isLoading }>
<GridItem fontWeight={ 600 }>Versioned hash</GridItem>
<GridItem overflow="hidden">
<BlobEntityL1 hash={ blob.hash }/>
</GridItem>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal">
{ dayjs(blob.l1_timestamp).fromNow() } | { dayjs(blob.l1_timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash</GridItem>
<GridItem overflow="hidden">
<TxEntityL1 hash={ blob.l1_transaction_hash } noIcon noCopy={ false }/>
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
}) }
</VStack>
);
};
export default React.memo(OptimisticL2TxnBatchBlobEip4844);
import { chakra, Grid, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobWrapper = ({ children, className, isLoading }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
return (
<Grid
className={ className }
columnGap={ 3 }
rowGap="10px"
p={ 4 }
bgColor={ bgColor }
gridTemplateColumns="auto 1fr"
borderRadius="base"
w="100%"
h={ isLoading ? '140px' : undefined }
fontSize="sm"
lineHeight={ 5 }
>
{ isLoading ? null : children }
</Grid>
);
};
export default React.memo(chakra(OptimisticL2TxnBatchBlobWrapper));
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { OptimismL2TxnBatch } from 'types/api/optimisticL2';
import type { ResourceError } from 'lib/api/resources';
import * as txnBatchesMock from 'mocks/l2txnBatches/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import OptimisticL2TxnBatchDetails from './OptimisticL2TxnBatchDetails';
const hooksConfig = {
router: {
query: { number: '1' },
},
};
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
});
test('call data blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeCallData,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test('celestia blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeCelestia,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test('EIP-4844 blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeEip4844,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
import { Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { OptimismL2TxnBatch } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import OptimisticL2TxnBatchBlobCallData from './OptimisticL2TxnBatchBlobCallData';
import OptimisticL2TxnBatchBlobCelestia from './OptimisticL2TxnBatchBlobCelestia';
import OptimisticL2TxnBatchBlobEip4844 from './OptimisticL2TxnBatchBlobEip4844';
interface Props {
query: UseQueryResult<OptimismL2TxnBatch, ResourceError>;
}
const OptimisticL2TxnBatchDetails = ({ query }: Props) => {
const router = useRouter();
const { data, isError, error, isPlaceholderData } = query;
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
}
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.internal_id + increment);
router.push({ pathname: '/batches/[number]', query: { number: nextId } }, undefined);
}, [ data, router ]);
if (isError) {
if (isCustomAppError(error)) {
throwOnResourceLoadError({ isError, error });
}
return <DataFetchAlert/>;
}
if (!data) {
return null;
}
const blocksCount = data.l2_block_end - data.l2_block_start + 1;
return (
<Grid
columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
overflow="hidden"
>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Batch ID indicates the length of batches produced by grouping L2 blocks to be proven on L1"
>
Batch ID
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.internal_id }
</Skeleton>
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous tx batch"
nextLabel="View next tx batch"
isPrevDisabled={ data.internal_id === 0 }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Date and time at which batch is submitted to L1"
>
Timestamp
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.l1_timestamp ?
<DetailsTimestamp timestamp={ data.l1_timestamp }isLoading={ isPlaceholderData }/> :
'Undefined'
}
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.internal_id.toString(), tab: 'txs' } }) }>
{ data.tx_count.toLocaleString() } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal>
{ ' ' }in this batch
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Number of L2 blocks in this batch"
>
Blocks
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.internal_id.toString(), tab: 'blocks' } }) }>
{ blocksCount.toLocaleString() } block{ blocksCount === 1 ? '' : 's' }
</LinkInternal>
{ ' ' }in this batch
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Where the batch data is stored"
>
Batch data container
</DetailsInfoItem.Label>
<DetailsInfoItem.Value flexDir="column" alignItems="flex-start" rowGap={ 2 }>
<OptimisticL2TxnBatchDA container={ data.batch_data_container } isLoading={ isPlaceholderData }/>
{ data.batch_data_container === 'in_blob4844' && data.blobs &&
<OptimisticL2TxnBatchBlobEip4844 blobs={ data.blobs } isLoading={ isPlaceholderData }/> }
{ data.batch_data_container === 'in_calldata' && (
<OptimisticL2TxnBatchBlobCallData
l1TxHashes={ data.l1_tx_hashes }
l1Timestamp={ data.l1_timestamp }
isLoading={ isPlaceholderData }
/>
) }
{ data.batch_data_container === 'in_celestia' && data.blobs &&
<OptimisticL2TxnBatchBlobCelestia blobs={ data.blobs } isLoading={ isPlaceholderData }/> }
</DetailsInfoItem.Value>
</Grid>
);
};
export default OptimisticL2TxnBatchDetails;
import { Skeleton, VStack } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2'; import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
...@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2'; ...@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2'; import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
...@@ -24,52 +24,60 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => { ...@@ -24,52 +24,60 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Batch ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value>
<BlockEntityL2 <BatchEntityL2 number={ item.internal_id } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
{ item.batch_data_container && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>
Storage
</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<OptimisticL2TxnBatchDA container={ item.batch_data_container } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading } isLoading={ isLoading }
number={ item.l2_block_number } display="inline-block"
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/> />
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block txn count</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.l1_tx_hashes.length }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) } href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'blocks' } }) }
isLoading={ isLoading } isLoading={ isLoading }
> >
<Skeleton isLoaded={ !isLoading } minW="40px"> <Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count } { item.l2_block_end - item.l2_block_start + 1 }
</Skeleton> </Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<VStack spacing={ 3 } w="100%" overflow="hidden" alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => (
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
)) }
</VStack>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<TimeAgoWithTooltip <LinkInternal
timestamp={ item.l1_timestamp } href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading } isLoading={ isLoading }
display="inline-block" >
/> <Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</ListItemMobileGrid.Container> </ListItemMobileGrid.Container>
......
...@@ -15,19 +15,21 @@ type Props = { ...@@ -15,19 +15,21 @@ type Props = {
const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => { const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="850px"> <Table variant="simple" size="sm" minW="850px" layout="auto">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="170px">L2 block #</Th> <Th>Batch ID</Th>
<Th width="170px">L2 block txn count</Th> <Th >Storage</Th>
<Th width="100%">L1 txn hash</Th> <Th >Age</Th>
<Th width="150px">Age</Th> <Th isNumeric>L1 txn count</Th>
<Th isNumeric>L2 blocks</Th>
<Th isNumeric>Txn</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item, index) => ( { items.map((item, index) => (
<OptimisticL2TxnBatchesTableItem <OptimisticL2TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') } key={ item.internal_id + (isLoading ? String(index) : '') }
item={ item } item={ item }
isLoading={ isLoading } isLoading={ isLoading }
/> />
......
import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react'; import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2'; import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
...@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2'; ...@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2'; import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
...@@ -22,49 +22,47 @@ const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => { ...@@ -22,49 +22,47 @@ const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
return ( return (
<Tr> <Tr>
<Td> <Td verticalAlign="middle">
<BlockEntityL2 <BatchEntityL2 number={ item.internal_id } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
{ item.batch_data_container ? <OptimisticL2TxnBatchDA container={ item.batch_data_container } isLoading={ isLoading }/> : '-' }
</Td>
<Td verticalAlign="middle">
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading } isLoading={ isLoading }
number={ item.l2_block_number } display="inline-block"
fontSize="sm" color="text_secondary"
lineHeight={ 5 } my={ 1 }
fontWeight={ 600 }
noIcon
/> />
</Td> </Td>
<Td> <Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.l1_tx_hashes.length }
</Skeleton>
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) } href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'blocks' } }) }
isLoading={ isLoading } isLoading={ isLoading }
justifyContent="flex-end"
> >
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }> <Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.tx_count } { item.l2_block_end - item.l2_block_start + 1 }
</Skeleton> </Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td pr={ 12 }> <Td verticalAlign="middle" isNumeric>
<VStack spacing={ 3 } alignItems="flex-start"> <LinkInternal
{ item.l1_tx_hashes.map(hash => ( href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
)) }
</VStack>
</Td>
<Td>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading } isLoading={ isLoading }
display="inline-block" justifyContent="flex-end"
color="text_secondary" >
my={ 1 } <Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
/> { item.tx_count }
</Skeleton>
</LinkInternal>
</Td> </Td>
</Tr> </Tr>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment