Commit 66af4e4c authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #2024 from blockscout/fe-2006

Arbitrum views
parents 52b1ee33 02a122b4
...@@ -10,6 +10,7 @@ on: ...@@ -10,6 +10,7 @@ on:
type: choice type: choice
options: options:
- none - none
- arbitrum
- base - base
- gnosis - gnosis
- eth - eth
......
# Set of ENVs for Optimism (dev only)
# https://optimism.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Arbitrum One Nitro
NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum One Nitro
NEXT_PUBLIC_NETWORK_ID=42161
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
# api configuration
NEXT_PUBLIC_API_HOST=arbitrum.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## views
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'}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
# rollup
NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype
\ No newline at end of file
...@@ -400,7 +400,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -400,7 +400,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ | | NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ | | NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | Required only for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ | | NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | Required only for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (OP stack only) | - | - | `true` | v1.31.0+ | | NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (OP stack only) | - | - | `true` | v1.31.0+ |
......
...@@ -33,6 +33,13 @@ import type { ...@@ -33,6 +33,13 @@ import type {
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type {
ArbitrumL2MessagesResponse,
ArbitrumL2TxnBatch,
ArbitrumL2TxnBatchesResponse,
ArbitrumL2BatchTxs,
ArbitrumL2BatchBlocks,
} from 'types/api/arbitrumL2';
import type { TxBlobs, Blob } from 'types/api/blobs'; import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
...@@ -663,6 +670,44 @@ export const RESOURCES = { ...@@ -663,6 +670,44 @@ export const RESOURCES = {
path: '/api/v2/optimism/games/count', path: '/api/v2/optimism/games/count',
}, },
// arbitrum L2
arbitrum_l2_messages: {
path: '/api/v2/arbitrum/messages/:direction',
pathParams: [ 'direction' as const ],
filterFields: [],
},
arbitrum_l2_messages_count: {
path: '/api/v2/arbitrum/messages/:direction/count',
pathParams: [ 'direction' as const ],
},
arbitrum_l2_txn_batches: {
path: '/api/v2/arbitrum/batches',
filterFields: [],
},
arbitrum_l2_txn_batches_count: {
path: '/api/v2/arbitrum/batches/count',
},
arbitrum_l2_txn_batch: {
path: '/api/v2/arbitrum/batches/:number',
pathParams: [ 'number' as const ],
},
arbitrum_l2_txn_batch_txs: {
path: '/api/v2/transactions/arbitrum-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
arbitrum_l2_txn_batch_blocks: {
path: '/api/v2/blocks/arbitrum-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
// zkEvm L2 // zkEvm L2
zkevm_l2_deposits: { zkevm_l2_deposits: {
path: '/api/v2/zkevm/deposits', path: '/api/v2/zkevm/deposits',
...@@ -868,6 +913,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -868,6 +913,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'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' |
'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' |
'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
...@@ -994,6 +1040,13 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : ...@@ -994,6 +1040,13 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_withdrawals_count' ? number :
Q extends 'shibarium_deposits_count' ? number : Q extends 'shibarium_deposits_count' ? number :
Q extends 'arbitrum_l2_messages' ? ArbitrumL2MessagesResponse :
Q extends 'arbitrum_l2_messages_count' ? number :
Q extends 'arbitrum_l2_txn_batches' ? ArbitrumL2TxnBatchesResponse :
Q extends 'arbitrum_l2_txn_batches_count' ? number :
Q extends 'arbitrum_l2_txn_batch' ? ArbitrumL2TxnBatch :
Q extends 'arbitrum_l2_txn_batch_txs' ? ArbitrumL2BatchTxs :
Q extends 'arbitrum_l2_txn_batch_blocks' ? ArbitrumL2BatchBlocks :
Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse : Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse :
Q extends 'zkevm_l2_deposits_count' ? number : Q extends 'zkevm_l2_deposits_count' ? number :
Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse : Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse :
......
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from 'types/api/arbitrumL2';
type Args = {
status: ArbitrumBatchStatus;
commitment_transaction: ArbitrumL2TxData;
confirmation_transaction: ArbitrumL2TxData;
}
export default function getArbitrumVerificationStepStatus({
status,
commitment_transaction: commitTx,
confirmation_transaction: confirmTx,
}: Args) {
if (status === 'Sent to base') {
if (commitTx.status === 'unfinalized') {
return 'pending';
}
}
if (status === 'Confirmed on base') {
if (confirmTx.status === 'unfinalized') {
return 'pending';
}
}
return 'finalized';
}
...@@ -105,7 +105,7 @@ export default function useNavItems(): ReturnType { ...@@ -105,7 +105,7 @@ export default function useNavItems(): ReturnType {
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'zkEvm')) { if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum' || rollupFeature.type === 'zkEvm')) {
blockchainNavItems = [ blockchainNavItems = [
[ [
txs, txs,
......
import type { ArbitrumL2MessagesResponse } from 'types/api/arbitrumL2';
export const baseResponse: ArbitrumL2MessagesResponse = {
items: [
{
completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e',
id: 181920,
origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a',
origination_transaction_block_number: 123456,
origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436',
origination_timestamp: '2023-06-01T14:46:48.000000Z',
status: 'initiated',
},
{
completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e',
id: 181921,
origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a',
origination_transaction_block_number: 123400,
origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436',
origination_timestamp: '2023-06-01T14:46:48.000000Z',
status: 'relayed',
},
],
next_page_params: {
items_count: 50,
id: 123,
direction: 'to-rollup',
},
};
import type { ArbitrumL2TxnBatch } from 'types/api/arbitrumL2';
import { finalized } from './txnBatches';
export const batchData: ArbitrumL2TxnBatch = {
...finalized,
after_acc: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb',
before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc',
start_block: 1245209,
end_block: 1245490,
};
import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatchesResponse } from 'types/api/arbitrumL2';
export const finalized: ArbitrumL2TxnBatchesItem = {
number: 12345,
block_count: 12345,
transactions_count: 10000,
commitment_transaction: {
block_number: 12345,
timestamp: '2022-04-17T08:51:58.000000Z',
hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661',
status: 'finalized',
},
};
export const unfinalized: ArbitrumL2TxnBatchesItem = {
number: 12344,
block_count: 10000,
transactions_count: 103020,
commitment_transaction: {
block_number: 12340,
timestamp: '2022-04-17T08:51:58.000000Z',
hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661',
status: 'unfinalized',
},
};
export const baseResponse: ArbitrumL2TxnBatchesResponse = {
items: [
finalized,
unfinalized,
],
next_page_params: {
items_count: 50,
number: 123,
},
};
import type { ArbitrumL2MessagesResponse } from 'types/api/arbitrumL2';
export const baseResponse: ArbitrumL2MessagesResponse = {
items: [
{
completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e',
id: 181920,
origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a',
origination_transaction_block_number: 123456,
origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436',
origination_timestamp: '2023-06-01T14:46:48.000000Z',
status: 'sent',
},
{
completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e',
id: 181921,
origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a',
origination_transaction_block_number: 123400,
origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436',
origination_timestamp: '2023-06-01T14:46:48.000000Z',
status: 'confirmed',
},
],
next_page_params: {
items_count: 50,
id: 123,
direction: 'from-rollup',
},
};
import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
import type { AdBannerProviders } from 'types/client/adProviders'; import type { AdBannerProviders } from 'types/client/adProviders';
import type { RollupType } from 'types/client/rollup';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
...@@ -66,8 +67,9 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => { ...@@ -66,8 +67,9 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => {
return account(context); return account(context);
}; };
const DEPOSITS_ROLLUP_TYPES: Array<RollupType> = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ];
export const deposits: GetServerSideProps<Props> = async(context) => { export const deposits: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm'))) { if (!(rollupFeature.isEnabled && DEPOSITS_ROLLUP_TYPES.includes(rollupFeature.type))) {
return { return {
notFound: true, notFound: true,
}; };
...@@ -76,10 +78,11 @@ export const deposits: GetServerSideProps<Props> = async(context) => { ...@@ -76,10 +78,11 @@ export const deposits: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
const WITHDRAWALS_ROLLUP_TYPES: Array<RollupType> = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ];
export const withdrawals: GetServerSideProps<Props> = async(context) => { export const withdrawals: GetServerSideProps<Props> = async(context) => {
if ( if (
!config.features.beaconChain.isEnabled && !config.features.beaconChain.isEnabled &&
!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm')) !(rollupFeature.isEnabled && WITHDRAWALS_ROLLUP_TYPES.includes(rollupFeature.type))
) { ) {
return { return {
notFound: true, notFound: true,
...@@ -109,8 +112,9 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => { ...@@ -109,8 +112,9 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum' ];
export const batch: GetServerSideProps<Props> = async(context) => { export const batch: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync'))) { if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) {
return { return {
notFound: true, notFound: true,
}; };
......
...@@ -15,6 +15,8 @@ const Batch = dynamic(() => { ...@@ -15,6 +15,8 @@ const Batch = dynamic(() => {
} }
switch (rollupFeature.type) { switch (rollupFeature.type) {
case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatch');
case 'zkEvm': case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatch'); return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync': case 'zkSync':
......
...@@ -19,6 +19,8 @@ const Batches = dynamic(() => { ...@@ -19,6 +19,8 @@ const Batches = dynamic(() => {
return import('ui/pages/ZkSyncL2TxnBatches'); return import('ui/pages/ZkSyncL2TxnBatches');
case 'optimistic': case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatches'); return import('ui/pages/OptimisticL2TxnBatches');
case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatches');
} }
throw new Error('Txn batches feature is not enabled.'); throw new Error('Txn batches feature is not enabled.');
}, { ssr: false }); }, { ssr: false });
......
...@@ -12,6 +12,10 @@ const Deposits = dynamic(() => { ...@@ -12,6 +12,10 @@ const Deposits = dynamic(() => {
return import('ui/pages/OptimisticL2Deposits'); return import('ui/pages/OptimisticL2Deposits');
} }
if (rollupFeature.isEnabled && rollupFeature.type === 'arbitrum') {
return import('ui/pages/ArbitrumL2Deposits');
}
if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') {
return import('ui/pages/ShibariumDeposits'); return import('ui/pages/ShibariumDeposits');
} }
......
...@@ -13,6 +13,10 @@ const Withdrawals = dynamic(() => { ...@@ -13,6 +13,10 @@ const Withdrawals = dynamic(() => {
return import('ui/pages/OptimisticL2Withdrawals'); return import('ui/pages/OptimisticL2Withdrawals');
} }
if (rollupFeature.isEnabled && rollupFeature.type === 'arbitrum') {
return import('ui/pages/ArbitrumL2Withdrawals');
}
if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') {
return import('ui/pages/ShibariumWithdrawals'); return import('ui/pages/ShibariumWithdrawals');
} }
......
...@@ -22,6 +22,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -22,6 +22,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
[ 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', 'https://localhost:3102' ], [ 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', 'https://localhost:3102' ],
[ 'NEXT_PUBLIC_FAULT_PROOF_ENABLED', 'true' ], [ 'NEXT_PUBLIC_FAULT_PROOF_ENABLED', 'true' ],
], ],
arbitrumRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'arbitrum' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
],
shibariumRollup: [ shibariumRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'shibarium' ], [ 'NEXT_PUBLIC_ROLLUP_TYPE', 'shibarium' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ], [ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
......
import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatch, ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import { ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx';
export const ARBITRUM_MESSAGES_ITEM: ArbitrumL2MessagesItem = {
completion_transaction_hash: TX_HASH,
id: 181920,
origination_address: ADDRESS_HASH,
origination_transaction_block_number: 123456,
origination_transaction_hash: TX_HASH,
origination_timestamp: '2023-06-01T14:46:48.000000Z',
status: 'relayed',
};
export const ARBITRUM_L2_TXN_BATCHES_ITEM: ArbitrumL2TxnBatchesItem = {
number: 12345,
block_count: 12345,
transactions_count: 10000,
commitment_transaction: {
block_number: 12345,
timestamp: '2024-04-17T08:51:58.000000Z',
hash: TX_HASH,
status: 'finalized',
},
};
export const ARBITRUM_L2_TXN_BATCH: ArbitrumL2TxnBatch = {
...ARBITRUM_L2_TXN_BATCHES_ITEM,
after_acc: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb',
before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc',
start_block: 1245209,
end_block: 1245490,
};
import type { Block } from './block';
import type { Transaction } from './transaction';
export type ArbitrumL2MessagesItem = {
completion_transaction_hash: string | null;
id: number;
origination_address: string;
origination_timestamp: string | null;
origination_transaction_block_number: number;
origination_transaction_hash: string;
status: 'initiated' | 'sent' | 'confirmed' | 'relayed';
}
export type ArbitrumL2MessagesResponse = {
items: Array<ArbitrumL2MessagesItem>;
next_page_params: {
direction: string;
id: number;
items_count: number;
};
}
export type ArbitrumL2TxData = {
hash: string | null;
status: string | null;
timestamp: string | null;
}
type ArbitrumL2BatchCommitmentTx = {
block_number: number;
hash: string;
status: string;
timestamp: string;
}
export type ArbitrumL2TxnBatchesItem = {
block_count: number;
commitment_transaction: ArbitrumL2BatchCommitmentTx;
number: number;
transactions_count: number;
}
export type ArbitrumL2TxnBatchesResponse = {
items: Array<ArbitrumL2TxnBatchesItem>;
next_page_params: {
number: number;
items_count: number;
} | null;
}
export type ArbitrumL2TxnBatch = {
after_acc: string;
before_acc: string;
commitment_transaction: ArbitrumL2BatchCommitmentTx;
end_block: number;
start_block: number;
number: number;
transactions_count: number;
}
export type ArbitrumL2BatchTxs = {
items: Array<Transaction>;
next_page_params: {
batch_number: string;
block_number: number;
index: number;
items_count: number;
} | null;
}
export type ArbitrumL2BatchBlocks = {
items: Array<Block>;
next_page_params: {
batch_number: string;
block_number: number;
items_count: number;
} | null;
}
export const ARBITRUM_L2_TX_BATCH_STATUSES = [
'Processed on rollup' as const,
'Sent to base' as const,
'Confirmed on base' as const,
];
export type ArbitrumBatchStatus = typeof ARBITRUM_L2_TX_BATCH_STATUSES[number];
...@@ -2,6 +2,7 @@ import type { AddressParam } from 'types/api/addressParams'; ...@@ -2,6 +2,7 @@ import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward'; 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 { ZkSyncBatchesItem } from './zkSyncL2'; import type { ZkSyncBatchesItem } from './zkSyncL2';
export type BlockType = 'block' | 'reorg' | 'uncle'; export type BlockType = 'block' | 'reorg' | 'uncle';
...@@ -48,6 +49,18 @@ export interface Block { ...@@ -48,6 +49,18 @@ export interface Block {
zksync?: Omit<ZkSyncBatchesItem, 'number' | 'tx_count' | 'timestamp'> & { zksync?: Omit<ZkSyncBatchesItem, 'number' | 'tx_count' | 'timestamp'> & {
'batch_number': number | null; 'batch_number': number | null;
}; };
arbitrum?: ArbitrumBlockData;
}
type ArbitrumBlockData = {
'batch_number': number;
'commitment_transaction': ArbitrumL2TxData;
'confirmation_transaction': ArbitrumL2TxData;
'delayed_messages': number;
'l1_block_height': number;
'send_count': number;
'send_root': string;
'status': ArbitrumBatchStatus;
} }
export interface BlocksResponse { export interface BlocksResponse {
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
import type { BlockTransactionsResponse } from './block'; import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
...@@ -93,6 +94,19 @@ export type Transaction = { ...@@ -93,6 +94,19 @@ export type Transaction = {
max_fee_per_blob_gas?: string; max_fee_per_blob_gas?: string;
// Noves-fi // Noves-fi
translation?: NovesTxTranslation; translation?: NovesTxTranslation;
arbitrum?: ArbitrumTransactionData;
}
type ArbitrumTransactionData = {
batch_number: number;
commitment_transaction: ArbitrumL2TxData;
confirmation_transaction: ArbitrumL2TxData;
contains_message: 'incoming' | 'outcoming' | null;
gas_used_for_l1: string;
gas_used_for_l2: string;
network_fee: string;
poster_fee: string;
status: ArbitrumBatchStatus;
} }
export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ]; export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ];
......
...@@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils'; ...@@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils';
export const ROLLUP_TYPES = [ export const ROLLUP_TYPES = [
'optimistic', 'optimistic',
'arbitrum',
'shibarium', 'shibarium',
'zkEvm', 'zkEvm',
'zkSync', 'zkSync',
......
...@@ -5,6 +5,7 @@ import { useRouter } from 'next/router'; ...@@ -5,6 +5,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import { ARBITRUM_L2_TX_BATCH_STATUSES } from 'types/api/arbitrumL2';
import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -12,6 +13,7 @@ import { route } from 'nextjs-routes'; ...@@ -12,6 +13,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import getBlockReward from 'lib/block/getBlockReward'; import getBlockReward from 'lib/block/getBlockReward';
import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts'; import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import getArbitrumVerificationStepStatus from 'lib/getArbitrumVerificationStepStatus';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -22,12 +24,14 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; ...@@ -22,12 +24,14 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp'; import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext'; import PrevNext from 'ui/shared/PrevNext';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import StatusTag from 'ui/shared/statusTag/StatusTag';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
...@@ -184,6 +188,22 @@ const BlockDetails = ({ query }: Props) => { ...@@ -184,6 +188,22 @@ const BlockDetails = ({ query }: Props) => {
/> />
</DetailsInfoItem.Value> </DetailsInfoItem.Value>
{ rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && data.arbitrum && !config.UI.views.block.hiddenFields?.batch && (
<>
<DetailsInfoItem.Label
hint="Batch number"
isLoading={ isPlaceholderData }
>
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.batch_number ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.arbitrum.batch_number }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItem.Label <DetailsInfoItem.Label
hint="Size of the block in bytes" hint="Size of the block in bytes"
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
...@@ -251,7 +271,9 @@ const BlockDetails = ({ query }: Props) => { ...@@ -251,7 +271,9 @@ const BlockDetails = ({ query }: Props) => {
</DetailsInfoItem.Value> </DetailsInfoItem.Value>
</> </>
) } ) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync && !config.UI.views.block.hiddenFields?.L1_status && ( { !config.UI.views.block.hiddenFields?.L1_status && rollupFeature.isEnabled &&
((rollupFeature.type === 'zkSync' && data.zksync) || (rollupFeature.type === 'arbitrum' && data.arbitrum)) &&
(
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
hint="Status is the short interpretation of the batch lifecycle" hint="Status is the short interpretation of the batch lifecycle"
...@@ -260,7 +282,16 @@ const BlockDetails = ({ query }: Props) => { ...@@ -260,7 +282,16 @@ const BlockDetails = ({ query }: Props) => {
Status Status
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES } currentStep={ data.zksync.status } isLoading={ isPlaceholderData }/> { rollupFeature.type === 'zkSync' && data.zksync &&
<VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES } currentStep={ data.zksync.status } isLoading={ isPlaceholderData }/> }
{ rollupFeature.type === 'arbitrum' && data.arbitrum && (
<VerificationSteps
steps={ ARBITRUM_L2_TX_BATCH_STATUSES }
currentStep={ data.arbitrum.status }
currentStepPending={ getArbitrumVerificationStepStatus(data.arbitrum) === 'pending' }
isLoading={ isPlaceholderData }
/>
) }
</DetailsInfoItem.Value> </DetailsInfoItem.Value>
</> </>
) } ) }
...@@ -282,6 +313,42 @@ const BlockDetails = ({ query }: Props) => { ...@@ -282,6 +313,42 @@ const BlockDetails = ({ query }: Props) => {
</> </>
) } ) }
{ rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' &&
(data.arbitrum?.commitment_transaction.hash || data.arbitrum?.confirmation_transaction.hash) &&
(
<>
<DetailsInfoItemDivider/>
{ data.arbitrum?.commitment_transaction.hash && (
<>
<DetailsInfoItem.Label
hint="L1 transaction containing this batch commitment"
isLoading={ isPlaceholderData }
>
Commitment tx
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<TxEntityL1 hash={ data.arbitrum?.commitment_transaction.hash } isLoading={ isPlaceholderData }/>
{ data.arbitrum?.commitment_transaction.status === 'finalized' && <StatusTag type="ok" text="Finalized" ml={ 2 }/> }
</DetailsInfoItem.Value>
</>
) }
{ data.arbitrum?.confirmation_transaction.hash && (
<>
<DetailsInfoItem.Label
hint="L1 transaction containing confirmation of this batch"
isLoading={ isPlaceholderData }
>
Confirmation tx
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<TxEntityL1 hash={ data.arbitrum?.confirmation_transaction.hash } isLoading={ isPlaceholderData }/>
{ data.arbitrum?.commitment_transaction.status === 'finalized' && <StatusTag type="ok" text="Finalized" ml={ 2 }/> }
</DetailsInfoItem.Value>
</>
) }
</>
) }
{ !rollupFeature.isEnabled && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && ( { !rollupFeature.isEnabled && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
......
...@@ -23,9 +23,11 @@ const TABS_HEIGHT = 88; ...@@ -23,9 +23,11 @@ const TABS_HEIGHT = 88;
interface Props { interface Props {
type?: BlockType; type?: BlockType;
query: QueryWithPagesResult<'blocks'>; query: QueryWithPagesResult<'blocks'>;
enableSocket?: boolean;
top?: number;
} }
const BlocksContent = ({ type, query }: Props) => { const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
...@@ -71,7 +73,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -71,7 +73,7 @@ const BlocksContent = ({ type, query }: Props) => {
topic: 'blocks:new_block', topic: 'blocks:new_block',
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: query.isPlaceholderData || query.isError || query.pagination.page !== 1, isDisabled: query.isPlaceholderData || query.isError || query.pagination.page !== 1 || !enableSocket,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -82,7 +84,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -82,7 +84,7 @@ const BlocksContent = ({ type, query }: Props) => {
const content = query.data?.items ? ( const content = query.data?.items ? (
<> <>
<Box display={{ base: 'block', lg: 'none' }}> <Box display={{ base: 'block', lg: 'none' }}>
{ query.pagination.page === 1 && ( { query.pagination.page === 1 && enableSocket && (
<SocketNewItemsNotice.Mobile <SocketNewItemsNotice.Mobile
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
...@@ -96,10 +98,10 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -96,10 +98,10 @@ const BlocksContent = ({ type, query }: Props) => {
<Box display={{ base: 'none', lg: 'block' }}> <Box display={{ base: 'none', lg: 'block' }}>
<BlocksTable <BlocksTable
data={ query.data.items } data={ query.data.items }
top={ query.pagination.isVisible ? TABS_HEIGHT : 0 } top={ top || (query.pagination.isVisible ? TABS_HEIGHT : 0) }
page={ query.pagination.page } page={ query.pagination.page }
isLoading={ query.isPlaceholderData } isLoading={ query.isPlaceholderData }
showSocketInfo={ query.pagination.page === 1 } showSocketInfo={ query.pagination.page === 1 && enableSocket }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
/> />
......
...@@ -70,7 +70,7 @@ const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => { ...@@ -70,7 +70,7 @@ const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => {
truncation="constant_long" truncation="constant_long"
/> />
) : ( ) : (
<chakra.span color="text_secondary"> <chakra.span>
Pending Claim Pending Claim
</chakra.span> </chakra.span>
) } ) }
......
import { Hide, Show, Skeleton } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import { ARBITRUM_MESSAGES_ITEM } from 'stubs/arbitrumL2';
import { generateListStub } from 'stubs/utils';
import ArbitrumL2MessagesListItem from 'ui/messages/ArbitrumL2MessagesListItem';
import ArbitrumL2MessagesTable from 'ui/messages/ArbitrumL2MessagesTable';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
export type MessagesDirection = 'from-rollup' | 'to-rollup';
type Props = {
direction: MessagesDirection;
}
const ArbitrumL2Messages = ({ direction }: Props) => {
const type = direction === 'from-rollup' ? 'withdrawals' : 'deposits';
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'arbitrum_l2_messages',
pathParams: { direction },
options: {
placeholderData: generateListStub<'arbitrum_l2_messages'>(
ARBITRUM_MESSAGES_ITEM,
50,
{ next_page_params: { items_count: 50, direction: 'to-rollup', id: 123456 } },
),
},
});
const countersQuery = useApiQuery('arbitrum_l2_messages_count', {
pathParams: { direction },
queryOptions: {
placeholderData: 1927029,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ArbitrumL2MessagesListItem
key={ String(item.id) + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
direction={ direction }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ArbitrumL2MessagesTable
items={ data.items }
direction={ direction }
top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ isPlaceholderData }
/>
</Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isError) {
return null;
}
return (
<Skeleton
isLoaded={ !countersQuery.isPlaceholderData }
display="inline-block"
>
A total of { countersQuery.data?.toLocaleString() } { type } found
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle
title={ direction === 'from-rollup' ?
`Withdrawals (L2${ nbsp }${ rightLineArrow }${ nbsp }L1)` :
`Deposits (L1${ nbsp }${ rightLineArrow }${ nbsp }L2)` }
withTextAd
/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText={ `There are no ${ type }.` }
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ArbitrumL2Messages;
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
import type { MessagesDirection } from './ArbitrumL2Messages';
const rollupFeature = config.features.rollup;
type Props = { item: ArbitrumL2MessagesItem; isLoading?: boolean; direction: MessagesDirection };
const ArbitrumL2MessagesListItem = ({ item, isLoading, direction }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
const timeAgo = dayjs(item.origination_timestamp).fromNow();
const l1TxHash = direction === 'from-rollup' ? item.completion_transaction_hash : item.origination_transaction_hash;
const l2TxHash = direction === 'from-rollup' ? item.origination_transaction_hash : item.completion_transaction_hash;
return (
<ListItemMobileGrid.Container gridTemplateColumns="110px auto">
{ direction === 'to-rollup' && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntityL1
number={ item.origination_transaction_block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
</>
) }
{ direction === 'from-rollup' && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
address={{ hash: item.origination_address }}
truncation="constant"
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Message #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.id }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 transaction</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ l2TxHash ? (
<TxEntity
isLoading={ isLoading }
hash={ l2TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : (
<chakra.span>
N/A
</chakra.span>
) }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 transaction</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ l1TxHash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ l1TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : (
<chakra.span>
N/A
</chakra.span>
) }
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ArbitrumL2MessagesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import type { MessagesDirection } from './ArbitrumL2Messages';
import ArbitrumL2MessagesTableItem from './ArbitrumL2MessagesTableItem';
type Props = {
items: Array<ArbitrumL2MessagesItem>;
direction: MessagesDirection;
top: number;
isLoading?: boolean;
}
const ArbitrumL2MessagesTable = ({ items, direction, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
{ direction === 'to-rollup' && <Th>L1 block</Th> }
{ direction === 'from-rollup' && <Th>From</Th> }
<Th>Message #</Th>
<Th>L2 transaction</Th>
<Th>Age</Th>
<Th>Status</Th>
<Th>L1 transaction</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ArbitrumL2MessagesTableItem
key={ String(item.id) + (isLoading ? index : '') }
item={ item }
direction={ direction }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default ArbitrumL2MessagesTable;
import { Td, Tr, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
import type { MessagesDirection } from './ArbitrumL2Messages';
const rollupFeature = config.features.rollup;
type Props = { item: ArbitrumL2MessagesItem; isLoading?: boolean; direction: MessagesDirection };
const ArbitrumL2MessagesTableItem = ({ item, direction, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
const timeAgo = dayjs(item.origination_timestamp).fromNow();
const l1TxHash = direction === 'from-rollup' ? item.completion_transaction_hash : item.origination_transaction_hash;
const l2TxHash = direction === 'from-rollup' ? item.origination_transaction_hash : item.completion_transaction_hash;
return (
<Tr>
{ direction === 'to-rollup' && (
<Td verticalAlign="middle">
<BlockEntityL1
number={ item.origination_transaction_block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
) }
{ direction === 'from-rollup' && (
<Td verticalAlign="middle">
<AddressEntity
address={{ hash: item.origination_address }}
truncation="constant"
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</Td>
) }
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>
<span>{ item.id }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
{ l2TxHash ? (
<TxEntity
isLoading={ isLoading }
hash={ l2TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
) : (
<chakra.span color="text_secondary">
N/A
</chakra.span>
) }
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
{ l1TxHash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ l1TxHash }
truncation="constant_long"
noIcon
fontSize="sm"
lineHeight={ 5 }
/>
) : (
<chakra.span color="text_secondary">
N/A
</chakra.span>
) }
</Td>
</Tr>
);
};
export default ArbitrumL2MessagesTableItem;
import React from 'react';
import * as depositsMock from 'mocks/arbitrum/deposits';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ArbitrumL2Deposits from './ArbitrumL2Deposits';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockApiResponse('arbitrum_l2_messages', depositsMock.baseResponse, { pathParams: { direction: 'to-rollup' } });
await mockApiResponse('arbitrum_l2_messages_count', 3971111, { pathParams: { direction: 'to-rollup' } });
const component = await render(<ArbitrumL2Deposits/>);
await expect(component).toHaveScreenshot();
});
import React from 'react';
import ArbitrumL2Messages from 'ui/messages/ArbitrumL2Messages';
const ArbitrumL2Deposits = () => {
return <ArbitrumL2Messages direction="to-rollup"/>;
};
export default ArbitrumL2Deposits;
import React from 'react';
import { batchData } from 'mocks/arbitrum/txnBatch';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib';
import ArbitrumL2TxnBatch from './ArbitrumL2TxnBatch';
const batchNumber = '5';
const hooksConfig = {
router: {
query: { number: batchNumber },
},
};
test.beforeEach(async({ mockTextAd, mockApiResponse, mockEnvs }) => {
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockTextAd();
await mockApiResponse('arbitrum_l2_txn_batch', batchData, { pathParams: { number: batchNumber } });
});
test('base view', async({ render }) => {
const component = await render(<ArbitrumL2TxnBatch/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render }) => {
const component = await render(<ArbitrumL2TxnBatch/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
});
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ARBITRUM_L2_TXN_BATCH } from 'stubs/arbitrumL2';
import { BLOCK } from 'stubs/block';
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 ArbitrumL2TxnBatchDetails from 'ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const TABS_HEIGHT = 80;
const ArbitrumL2TxnBatch = () => {
const router = useRouter();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
const tab = getQueryParamString(router.query.tab);
const isMobile = useIsMobile();
const batchQuery = useApiQuery('arbitrum_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: ARBITRUM_L2_TXN_BATCH,
},
});
const batchTxsQuery = useQueryWithPages({
resourceName: 'arbitrum_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'txs'),
placeholderData: generateListStub<'arbitrum_l2_txn_batch_txs'>(TX, 50, { next_page_params: {
batch_number: '8122',
block_number: 1338932,
index: 0,
items_count: 50,
} }),
},
});
const batchBlocksQuery = useQueryWithPages({
resourceName: 'arbitrum_l2_txn_batch_blocks',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'blocks'),
placeholderData: generateListStub<'arbitrum_l2_txn_batch_blocks'>(BLOCK, 50, { next_page_params: {
batch_number: '8122',
block_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: <ArbitrumL2TxnBatchDetails 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={ `Tx 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 ArbitrumL2TxnBatch;
import React from 'react';
import * as arbitrumTxnBatchesMock from 'mocks/arbitrum/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ArbitrumL2TxnBatches from './ArbitrumL2TxnBatches';
test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => {
test.slow();
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockTextAd();
await mockApiResponse('arbitrum_l2_txn_batches', arbitrumTxnBatchesMock.baseResponse);
await mockApiResponse('arbitrum_l2_txn_batches_count', 9927);
const component = await render(<ArbitrumL2TxnBatches/>);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { nbsp } from 'lib/html-entities';
import { ARBITRUM_L2_TXN_BATCHES_ITEM } from 'stubs/arbitrumL2';
import { generateListStub } from 'stubs/utils';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import ArbitrumL2TxnBatchesListItem from 'ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesListItem';
import ArbitrumL2TxnBatchesTable from 'ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesTable';
const ArbitrumL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'arbitrum_l2_txn_batches',
options: {
placeholderData: generateListStub<'arbitrum_l2_txn_batches'>(
ARBITRUM_L2_TXN_BATCHES_ITEM,
50,
{
next_page_params: {
items_count: 50,
number: 9045200,
},
},
),
},
});
const countersQuery = useApiQuery('arbitrum_l2_txn_batches_count', {
queryOptions: {
placeholderData: 5231746,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ArbitrumL2TxnBatchesListItem
key={ item.number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ArbitrumL2TxnBatchesTable items={ data.items } top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isError || isError || !data?.items.length) {
return null;
}
return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text>
(total of { countersQuery.data?.toLocaleString() } batches)
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no tx batches."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ArbitrumL2TxnBatches;
import React from 'react';
import * as depositsMock from 'mocks/arbitrum/withdrawals';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ArbitrumL2Withdrawals from './ArbitrumL2Withdrawals';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockApiResponse('arbitrum_l2_messages', depositsMock.baseResponse, { pathParams: { direction: 'from-rollup' } });
await mockApiResponse('arbitrum_l2_messages_count', 3971111, { pathParams: { direction: 'from-rollup' } });
const component = await render(<ArbitrumL2Withdrawals/>);
await expect(component).toHaveScreenshot();
});
import React from 'react';
import ArbitrumL2Messages from 'ui/messages/ArbitrumL2Messages';
const ArbitrumL2Withdrawals = () => {
return <ArbitrumL2Messages direction="from-rollup"/>;
};
export default ArbitrumL2Withdrawals;
...@@ -47,6 +47,11 @@ const BlockPageContent = () => { ...@@ -47,6 +47,11 @@ const BlockPageContent = () => {
const blockWithdrawalsQuery = useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab }); const blockWithdrawalsQuery = useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab });
const blockBlobTxsQuery = useBlockBlobTxsQuery({ heightOrHash, blockQuery, tab }); const blockBlobTxsQuery = useBlockBlobTxsQuery({ heightOrHash, blockQuery, tab });
const hasPagination = !isMobile && (
(tab === 'txs' && blockTxsQuery.pagination.isVisible) ||
(tab === 'withdrawals' && blockWithdrawalsQuery.pagination.isVisible)
);
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ {
id: 'index', id: 'index',
...@@ -64,7 +69,7 @@ const BlockPageContent = () => { ...@@ -64,7 +69,7 @@ const BlockPageContent = () => {
component: ( component: (
<> <>
{ blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> } { blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> }
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false } top={ TABS_HEIGHT }/> <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
</> </>
), ),
}, },
...@@ -87,12 +92,7 @@ const BlockPageContent = () => { ...@@ -87,12 +92,7 @@ const BlockPageContent = () => {
</> </>
), ),
} : null, } : null,
].filter(Boolean)), [ blockBlobTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery ]); ].filter(Boolean)), [ blockBlobTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination ]);
const hasPagination = !isMobile && (
(tab === 'txs' && blockTxsQuery.pagination.isVisible) ||
(tab === 'withdrawals' && blockWithdrawalsQuery.pagination.isVisible)
);
let pagination; let pagination;
if (tab === 'txs') { if (tab === 'txs') {
......
...@@ -27,6 +27,8 @@ const TAB_LIST_PROPS = { ...@@ -27,6 +27,8 @@ const TAB_LIST_PROPS = {
marginTop: -5, marginTop: -5,
}; };
const TABS_HEIGHT = 80;
const ZkSyncL2TxnBatch = () => { const ZkSyncL2TxnBatch = () => {
const router = useRouter(); const router = useRouter();
const appProps = useAppContext(); const appProps = useAppContext();
...@@ -59,10 +61,16 @@ const ZkSyncL2TxnBatch = () => { ...@@ -59,10 +61,16 @@ const ZkSyncL2TxnBatch = () => {
throwOnAbsentParamError(number); throwOnAbsentParamError(number);
throwOnResourceLoadError(batchQuery); throwOnResourceLoadError(batchQuery);
const hasPagination = !isMobile && batchTxsQuery.pagination.isVisible && tab === 'txs';
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkSyncL2TxnBatchDetails query={ batchQuery }/> }, { id: 'index', title: 'Details', component: <ZkSyncL2TxnBatchDetails query={ batchQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false }/> }, {
].filter(Boolean)), [ batchQuery, batchTxsQuery ]); id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
].filter(Boolean)), [ batchQuery, batchTxsQuery, hasPagination ]);
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches'); const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches');
...@@ -77,8 +85,6 @@ const ZkSyncL2TxnBatch = () => { ...@@ -77,8 +85,6 @@ const ZkSyncL2TxnBatch = () => {
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
const hasPagination = !isMobile && batchTxsQuery.pagination.isVisible && tab === 'txs';
return ( return (
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
......
import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
export interface Props {
status: ArbitrumL2MessagesItem['status'];
isLoading?: boolean;
}
const ArbitrumL2MessageStatus = ({ status, isLoading }: Props) => {
let type: StatusTagType;
switch (status) {
case 'relayed':
case 'confirmed':
type = 'ok';
break;
default:
type = 'pending';
break;
}
return <StatusTag type={ type } text={ status } isLoading={ isLoading }/>;
};
export default ArbitrumL2MessageStatus;
import React from 'react';
import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
export interface Props {
status: ArbitrumL2TxnBatchesItem['commitment_transaction']['status'];
isLoading?: boolean;
}
const ArbitrumL2TxnBatchStatus = ({ status, isLoading }: Props) => {
let type: StatusTagType;
switch (status) {
case 'finalized':
type = 'ok';
break;
default:
type = 'pending';
break;
}
return <StatusTag type={ type } text={ status } isLoading={ isLoading }/>;
};
export default ArbitrumL2TxnBatchStatus;
import { TagLabel, Tooltip } from '@chakra-ui/react'; import { TagLabel, Tooltip, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
...@@ -12,12 +12,15 @@ export interface Props { ...@@ -12,12 +12,15 @@ export interface Props {
text: string; text: string;
errorText?: string | null; errorText?: string | null;
isLoading?: boolean; isLoading?: boolean;
className?: string;
} }
const StatusTag = ({ type, text, errorText, isLoading }: Props) => { const StatusTag = ({ type, text, errorText, isLoading, className }: Props) => {
let icon: IconName; let icon: IconName;
let colorScheme; let colorScheme;
const capitalizedText = text.charAt(0).toUpperCase() + text.slice(1);
switch (type) { switch (type) {
case 'ok': case 'ok':
icon = 'status/success'; icon = 'status/success';
...@@ -35,12 +38,12 @@ const StatusTag = ({ type, text, errorText, isLoading }: Props) => { ...@@ -35,12 +38,12 @@ const StatusTag = ({ type, text, errorText, isLoading }: Props) => {
return ( return (
<Tooltip label={ errorText }> <Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="flex" isLoading={ isLoading } > <Tag colorScheme={ colorScheme } display="flex" isLoading={ isLoading } className={ className }>
<IconSvg boxSize={ 2.5 } name={ icon } mr={ 1 } flexShrink={ 0 }/> <IconSvg boxSize={ 2.5 } name={ icon } mr={ 1 } flexShrink={ 0 }/>
<TagLabel display="block">{ text }</TagLabel> <TagLabel display="block">{ capitalizedText }</TagLabel>
</Tag> </Tag>
</Tooltip> </Tooltip>
); );
}; };
export default StatusTag; export default chakra(StatusTag);
...@@ -9,10 +9,16 @@ type Props = { ...@@ -9,10 +9,16 @@ type Props = {
step: Step; step: Step;
isLast: boolean; isLast: boolean;
isPassed: boolean; isPassed: boolean;
isPending?: boolean;
} }
const VerificationStep = ({ step, isLast, isPassed }: Props) => { const VerificationStep = ({ step, isLast, isPassed, isPending }: Props) => {
const stepColor = isPassed ? 'green.500' : 'text_secondary'; let stepColor = 'text_secondary';
if (isPending) {
stepColor = 'yellow.500';
} else if (isPassed) {
stepColor = 'green.500';
}
return ( return (
<HStack gap={ 2 } color={ stepColor }> <HStack gap={ 2 } color={ stepColor }>
......
...@@ -7,13 +7,14 @@ import VerificationStep from './VerificationStep'; ...@@ -7,13 +7,14 @@ import VerificationStep from './VerificationStep';
export interface Props { export interface Props {
currentStep: string; currentStep: string;
currentStepPending?: boolean;
steps: Array<Step>; steps: Array<Step>;
isLoading?: boolean; isLoading?: boolean;
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
className?: string; className?: string;
} }
const VerificationSteps = ({ currentStep, steps, isLoading, rightSlot, className }: Props) => { const VerificationSteps = ({ currentStep, currentStepPending, steps, isLoading, rightSlot, className }: Props) => {
const currentStepIndex = steps.findIndex((step) => { const currentStepIndex = steps.findIndex((step) => {
const label = typeof step === 'string' ? step : step.label; const label = typeof step === 'string' ? step : step.label;
return label === currentStep; return label === currentStep;
...@@ -34,6 +35,7 @@ const VerificationSteps = ({ currentStep, steps, isLoading, rightSlot, className ...@@ -34,6 +35,7 @@ const VerificationSteps = ({ currentStep, steps, isLoading, rightSlot, className
step={ step } step={ step }
isLast={ index === steps.length - 1 && !rightSlot } isLast={ index === steps.length - 1 && !rightSlot }
isPassed={ index <= currentStepIndex } isPassed={ index <= currentStepIndex }
isPending={ index === currentStepIndex && currentStepPending }
/> />
)) } )) }
{ rightSlot } { rightSlot }
......
...@@ -15,6 +15,7 @@ import BigNumber from 'bignumber.js'; ...@@ -15,6 +15,7 @@ import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import { ARBITRUM_L2_TX_BATCH_STATUSES } from 'types/api/arbitrumL2';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction'; import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction';
import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
...@@ -23,6 +24,7 @@ import { route } from 'nextjs-routes'; ...@@ -23,6 +24,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { WEI, WEI_IN_GWEI } from 'lib/consts'; import { WEI, WEI_IN_GWEI } from 'lib/consts';
import getArbitrumVerificationStepStatus from 'lib/getArbitrumVerificationStepStatus';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration'; import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
...@@ -36,10 +38,12 @@ import DetailsTimestamp from 'ui/shared/DetailsTimestamp'; ...@@ -36,10 +38,12 @@ import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData'; import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
import StatusTag from 'ui/shared/statusTag/StatusTag';
import TxStatus from 'ui/shared/statusTag/TxStatus'; import TxStatus from 'ui/shared/statusTag/TxStatus';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxFeeStability from 'ui/shared/tx/TxFeeStability';
...@@ -152,7 +156,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -152,7 +156,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
isLoading={ isLoading } isLoading={ isLoading }
> >
{ {
rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync') ? rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum') ?
'L2 status and method' : 'L2 status and method' :
'Status and method' 'Status and method'
} }
...@@ -164,6 +168,11 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -164,6 +168,11 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
{ data.method } { data.method }
</Tag> </Tag>
) } ) }
{ data.arbitrum?.contains_message && (
<Tag isLoading={ isLoading } isTruncated ml={ 3 }>
{ data.arbitrum?.contains_message === 'incoming' ? 'Incoming message' : 'Outgoing message' }
</Tag>
) }
</DetailsInfoItem.Value> </DetailsInfoItem.Value>
{ rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.op_withdrawals && data.op_withdrawals.length > 0 && { rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.op_withdrawals && data.op_withdrawals.length > 0 &&
...@@ -172,7 +181,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -172,7 +181,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<DetailsInfoItem.Label <DetailsInfoItem.Label
hint="Detailed status progress of the transaction" hint="Detailed status progress of the transaction"
> >
Withdrawal status Withdrawal status
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<Flex flexDir="column" rowGap={ 2 }> <Flex flexDir="column" rowGap={ 2 }>
...@@ -207,6 +216,25 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -207,6 +216,25 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</> </>
) } ) }
{ data.arbitrum?.status && !config.UI.views.tx.hiddenFields?.L1_status && (
<>
<DetailsInfoItem.Label
hint="Status of the transaction confirmation path to L1"
isLoading={ isLoading }
>
L1 status
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<VerificationSteps
currentStep={ data.arbitrum.status }
currentStepPending={ getArbitrumVerificationStepStatus(data.arbitrum) === 'pending' }
steps={ ARBITRUM_L2_TX_BATCH_STATUSES }
isLoading={ isLoading }
/>
</DetailsInfoItem.Value>
</>
) }
{ data.revert_reason && ( { data.revert_reason && (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
...@@ -226,7 +254,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -226,7 +254,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
hint="Status is the short interpretation of the batch lifecycle" hint="Status is the short interpretation of the batch lifecycle"
isLoading={ isLoading } isLoading={ isLoading }
> >
L1 status L1 status
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES } currentStep={ data.zksync.status } isLoading={ isLoading }/> <VerificationSteps steps={ ZKSYNC_L2_TX_BATCH_STATUSES } currentStep={ data.zksync.status } isLoading={ isLoading }/>
...@@ -295,6 +323,22 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -295,6 +323,22 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</> </>
) } ) }
{ data.arbitrum && !config.UI.views.tx.hiddenFields?.batch && (
<>
<DetailsInfoItem.Label
hint="Index of the batch containing this transaction"
isLoading={ isLoading }
>
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.batch_number ?
<BatchEntityL2 isLoading={ isLoading } number={ data.arbitrum.batch_number }/> :
<Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
</DetailsInfoItem.Value>
</>
) }
{ data.timestamp && ( { data.timestamp && (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
...@@ -412,6 +456,41 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -412,6 +456,41 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<DetailsInfoItemDivider/> <DetailsInfoItemDivider/>
{ (data.arbitrum?.commitment_transaction.hash || data.arbitrum?.confirmation_transaction.hash) &&
(
<>
{ data.arbitrum?.commitment_transaction.hash && (
<>
<DetailsInfoItem.Label
hint="L1 transaction containing this batch commitment"
isLoading={ isLoading }
>
Commitment tx
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<TxEntityL1 hash={ data.arbitrum?.commitment_transaction.hash } isLoading={ isLoading }/>
{ data.arbitrum?.commitment_transaction.status === 'finalized' && <StatusTag type="ok" text="Finalized" ml={ 2 }/> }
</DetailsInfoItem.Value>
</>
) }
{ data.arbitrum?.confirmation_transaction.hash && (
<>
<DetailsInfoItem.Label
hint="L1 transaction containing confirmation of this batch"
isLoading={ isLoading }
>
Confirmation tx
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<TxEntityL1 hash={ data.arbitrum?.confirmation_transaction.hash } isLoading={ isLoading }/>
{ data.arbitrum?.commitment_transaction.status === 'finalized' && <StatusTag type="ok" text="Finalized" ml={ 2 }/> }
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItemDivider/>
</>
) }
{ data.zkevm_sequence_hash && ( { data.zkevm_sequence_hash && (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
......
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 { ArbitrumL2TxnBatch } from 'types/api/arbitrumL2';
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 CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
interface Props {
query: UseQueryResult<ArbitrumL2TxnBatch, ResourceError>;
}
const ArbitrumL2TxnBatchDetails = ({ query }: Props) => {
const router = useRouter();
const { data, isPlaceholderData, isError, error } = query;
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
}
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.number + increment);
router.push({ pathname: '/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.end_block - data.start_block + 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 number indicates the length of batches produced by grouping L2 blocks to be proven on L1"
>
Tx batch number
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.number }
</Skeleton>
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous tx batch"
nextLabel="View next tx batch"
isPrevDisabled={ data.number === 0 }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Date and time at which batch is submitted to L1"
>
Timestamp
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.commitment_transaction.timestamp ?
<DetailsTimestamp timestamp={ data.commitment_transaction.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.number.toString(), tab: 'txs' } }) }>
{ data.transactions_count } transaction{ data.transactions_count === 1 ? '' : 's' }
</LinkInternal>
</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.number.toString(), tab: 'blocks' } }) }>
{ blocksCount } block{ blocksCount === 1 ? '' : 's' }
</LinkInternal>
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Hash of L1 transaction in which transactions was committed"
>
L1 transaction hash
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.commitment_transaction.hash }
maxW="100%"
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Heigh of L1 block which includes L1 transactions"
>
L1 block
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<BlockEntityL1
isLoading={ isPlaceholderData }
number={ data.commitment_transaction.block_number }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="The hash of the state before the batch"
>
Before acc
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.before_acc }/>
<CopyToClipboard text={ data.before_acc }/>
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="The hash of the state after the batch"
>
After acc
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.after_acc }/>
<CopyToClipboard text={ data.after_acc }/>
</Skeleton>
</DetailsInfoItem.Value>
</Grid>
);
};
export default ArbitrumL2TxnBatchDetails;
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ArbitrumL2TxnBatchStatus from 'ui/shared/statusTag/ArbitrumL2TxnBatchStatus';
const rollupFeature = config.features.rollup;
type Props = { item: ArbitrumL2TxnBatchesItem; isLoading?: boolean };
const ArbitrumL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = item.commitment_transaction.timestamp ? dayjs(item.commitment_transaction.timestamp).fromNow() : 'Undefined';
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
return (
<ListItemMobileGrid.Container gridTemplateColumns="110px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ArbitrumL2TxnBatchStatus status={ item.commitment_transaction.status } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntityL1
number={ item.commitment_transaction.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
</ListItemMobileGrid.Value>
{ item.block_count && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.block_count }</Skeleton>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 transaction</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntityL1
hash={ item.commitment_transaction.hash }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
fontWeight={ 600 }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.transactions_count }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ArbitrumL2TxnBatchesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import ArbitrumL2TxnBatchesTableItem from './ArbitrumL2TxnBatchesTableItem';
type Props = {
items: Array<ArbitrumL2TxnBatchesItem>;
top: number;
isLoading?: boolean;
}
const ArbitrumL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Batch #</Th>
<Th>L1 status</Th>
<Th>L1 block</Th>
<Th>L2 block txn count</Th>
<Th>L1 transaction</Th>
<Th>Age</Th>
<Th>Txn count</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ArbitrumL2TxnBatchesTableItem
key={ item.number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default ArbitrumL2TxnBatchesTable;
import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ArbitrumL2TxnBatchStatus from 'ui/shared/statusTag/ArbitrumL2TxnBatchStatus';
const rollupFeature = config.features.rollup;
type Props = { item: ArbitrumL2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = item.commitment_transaction.timestamp ? dayjs(item.commitment_transaction.timestamp).fromNow() : 'Undefined';
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
return (
<Tr>
<Td verticalAlign="middle">
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
<ArbitrumL2TxnBatchStatus status={ item.commitment_transaction.status } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<BlockEntityL1
number={ item.commitment_transaction.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.block_count || 'N/A' }</Skeleton>
</Td>
<Td pr={ 12 } verticalAlign="middle">
<TxEntityL1
hash={ item.commitment_transaction.hash }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.transactions_count }
</Skeleton>
</LinkInternal>
</Td>
</Tr>
);
};
export default TxnBatchesTableItem;
...@@ -70,7 +70,7 @@ const ZkEvmL2WithdrawalsListItem = ({ item, isLoading }: Props) => { ...@@ -70,7 +70,7 @@ const ZkEvmL2WithdrawalsListItem = ({ item, isLoading }: Props) => {
truncation="constant_long" truncation="constant_long"
/> />
) : ( ) : (
<chakra.span color="text_secondary"> <chakra.span>
Pending Claim Pending Claim
</chakra.span> </chakra.span>
) } ) }
......
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