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:
- eth_sepolia
- eth_goerli
- optimism
- optimism_celestia
- optimism_sepolia
- polygon
- rootstock
......
......@@ -20,6 +20,7 @@ on:
- eth_sepolia
- eth_goerli
- optimism
- optimism_celestia
- optimism_sepolia
- polygon
- rootstock
......
......@@ -368,6 +368,7 @@
"eth_goerli",
"eth_sepolia",
"optimism",
"optimism_celestia",
"optimism_sepolia",
"polygon",
"rootstock_testnet",
......
......@@ -2,6 +2,8 @@ import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { getEnvValue } from '../utils';
const type = (() => {
......@@ -21,7 +23,7 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s
title,
isEnabled: true,
type,
L1BaseUrl,
L1BaseUrl: stripTrailingSlash(L1BaseUrl),
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 {
OptimisticL2TxnBatchesResponse,
OptimisticL2WithdrawalsResponse,
OptimisticL2DisputeGamesResponse,
OptimismL2TxnBatch,
OptimismL2BatchTxs,
OptimismL2BatchBlocks,
} from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
......@@ -678,12 +681,29 @@ export const RESOURCES = {
},
optimistic_l2_txn_batches: {
path: '/api/v2/optimism/txn-batches',
path: '/api/v2/optimism/batches',
filterFields: [],
},
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: {
......@@ -967,7 +987,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
'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' |
'shibarium_deposits' | 'shibarium_withdrawals' |
'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 :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
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_output_roots_count' ? number :
Q extends 'optimistic_l2_withdrawals_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 :
never;
// !!! IMPORTANT !!!
......
export const txnBatchesData = {
import type {
OptimismL2TxnBatchTypeCallData,
OptimismL2TxnBatchTypeCelestia,
OptimismL2TxnBatchTypeEip4844,
OptimisticL2TxnBatchesResponse,
} from 'types/api/optimisticL2';
export const txnBatchesData: OptimisticL2TxnBatchesResponse = {
items: [
{
batch_data_container: 'in_blob4844',
internal_id: 260998,
l1_timestamp: '2022-11-10T11:29:11.000000Z',
l1_tx_hashes: [
'0x5bc94d02b65743dfaa9e10a2d6e175aff2a05cce2128c8eaf848bd84ab9325c5',
'0x92a51bc623111dbb91f243e3452e60fab6f090710357f9d9b75ac8a0f67dfd9d',
'0x9553351f6bd1577f4e782738c087be08697fb11f3b91745138d71ba166d62c3b',
],
l1_timestamp: '2023-02-24T10:16:12.000000Z',
l2_block_number: 5902836,
tx_count: 0,
l2_block_end: 124882074,
l2_block_start: 124881833,
tx_count: 4011,
},
{
batch_data_container: 'in_calldata',
internal_id: 260997,
l1_timestamp: '2022-11-03T11:20:59.000000Z',
l1_tx_hashes: [
'0xc45f846ee28ce9ba116ce2d378d3dd00b55d324b833b3ecd4241c919c572c4aa',
'0x80f5fba70d5685bc2b70df836942e892b24afa7bba289a2fac0ca8f4d554cc72',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902835,
tx_count: 0,
l2_block_end: 124881832,
l2_block_start: 124881613,
tx_count: 4206,
},
{
internal_id: 260996,
l1_timestamp: '2024-09-03T11:14:23.000000Z',
l1_tx_hashes: [
'0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
'0x39f4c46cae57bae936acb9159e367794f41f021ed3788adb80ad93830edb5f22',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902834,
tx_count: 0,
l2_block_end: 124881612,
l2_block_start: 124881380,
tx_count: 4490,
},
],
next_page_params: {
block_number: 5902834,
id: 5902834,
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) => {
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) => {
if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) {
return {
......
......@@ -17,6 +17,8 @@ const Batch = dynamic(() => {
switch (rollupFeature.type) {
case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatch');
case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatch');
case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync':
......
......@@ -24,6 +24,7 @@
| "block_slim"
| "block"
| "brands/blockscout"
| "brands/celenium"
| "brands/safe"
| "brands/solidity_scan"
| "burger"
......
import type {
OptimismL2TxnBatch,
OptimisticL2DepositsItem,
OptimisticL2DisputeGamesItem,
OptimisticL2OutputRootsItem,
......@@ -30,14 +31,29 @@ export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = {
};
export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = {
internal_id: 260991,
batch_data_container: 'in_blob4844',
l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [
TX_HASH,
],
l2_block_number: 5218590,
l2_block_start: 5218590,
l2_block_end: 5218777,
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 = {
l1_block_number: 9103684,
l1_timestamp: '2023-06-01T15:26:12.000000Z',
......
......@@ -11,6 +11,7 @@ const PRESETS = {
eth_sepolia: 'https://eth-sepolia.blockscout.com',
gnosis: 'https://gnosis.blockscout.com',
optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
polygon: 'https://polygon.blockscout.com',
rootstock_testnet: 'https://rootstock-testnet.blockscout.com',
......
......@@ -3,6 +3,7 @@ import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { ZkSyncBatchesItem } from './zkSyncL2';
......@@ -59,6 +60,7 @@ export interface Block {
'batch_number': number | null;
};
arbitrum?: ArbitrumBlockData;
optimism?: OptimismBlockData;
// CELO FIELDS
celo?: {
epoch_number: number;
......@@ -78,6 +80,14 @@ type ArbitrumBlockData = {
'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 {
items: Array<Block>;
next_page_params: {
......
import type { AddressParam } from './addressParams';
import type { Block } from './block';
import type { Transaction } from './transaction';
export type OptimisticL2DepositsItem = {
l1_block_number: number;
......@@ -35,21 +37,82 @@ export type OptimisticL2OutputRootsResponse = {
};
}
export type OptimisticL2BatchDataContainer = 'in_blob4844' | 'in_celestia' | 'in_calldata';
export type OptimisticL2TxnBatchesItem = {
l1_tx_hashes: Array<string>;
internal_id: number;
batch_data_container?: OptimisticL2BatchDataContainer;
l1_timestamp: string;
l2_block_number: number;
l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number;
}
export type OptimisticL2TxnBatchesResponse = {
items: Array<OptimisticL2TxnBatchesItem>;
next_page_params: {
block_number: number;
id: 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 = {
'challenge_period_end': string | null;
'from': AddressParam | null;
......
......@@ -19,6 +19,7 @@ import getNetworkValidationActionText from 'lib/networks/getNetworkValidationAct
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
......@@ -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
hint="Size of the block in bytes"
isLoading={ isPlaceholderData }
......
......@@ -26,7 +26,7 @@ const TABS_HEIGHT = 88;
interface Props {
type?: BlockType;
query: QueryWithPagesResult<'blocks'>;
query: QueryWithPagesResult<'blocks'> | QueryWithPagesResult<'optimistic_l2_txn_batch_blocks'>;
enableSocket?: boolean;
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 = () => {
{
next_page_params: {
items_count: 50,
block_number: 9045200,
id: 9045200,
},
},
),
......@@ -41,7 +41,7 @@ const OptimisticL2TxnBatches = () => {
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<OptimisticL2TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') }
key={ item.internal_id + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
......@@ -61,8 +61,8 @@ const OptimisticL2TxnBatches = () => {
return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].internal_id } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].internal_id } </Text>
(total of { countersQuery.data?.toLocaleString() } batches)
</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 type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
......@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -24,52 +24,60 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<BlockEntityL2
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<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 }
number={ item.l2_block_number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
display="inline-block"
/>
</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>
<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 }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
{ item.l2_block_end - item.l2_block_start + 1 }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</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.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
display="inline-block"
/>
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
......
......@@ -15,19 +15,21 @@ type Props = {
const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Table variant="simple" size="sm" minW="850px" layout="auto">
<Thead top={ top }>
<Tr>
<Th width="170px">L2 block #</Th>
<Th width="170px">L2 block txn count</Th>
<Th width="100%">L1 txn hash</Th>
<Th width="150px">Age</Th>
<Th>Batch ID</Th>
<Th >Storage</Th>
<Th >Age</Th>
<Th isNumeric>L1 txn count</Th>
<Th isNumeric>L2 blocks</Th>
<Th isNumeric>Txn</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<OptimisticL2TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') }
key={ item.internal_id + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
......
import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react';
import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
......@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -22,49 +22,47 @@ const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
return (
<Tr>
<Td>
<BlockEntityL2
<Td verticalAlign="middle">
<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 }
number={ item.l2_block_number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
display="inline-block"
color="text_secondary"
my={ 1 }
/>
</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
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 }
justifyContent="flex-end"
>
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.l2_block_end - item.l2_block_start + 1 }
</Skeleton>
</LinkInternal>
</Td>
<Td pr={ 12 }>
<VStack spacing={ 3 } alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => (
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
)) }
</VStack>
</Td>
<Td>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
display="inline-block"
color="text_secondary"
my={ 1 }
/>
justifyContent="flex-end"
>
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</Td>
</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