Commit 820d0096 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Scroll views (#2418)

* Scroll views

* review fix

* fix tests
parent f3481e6f
...@@ -25,6 +25,7 @@ on: ...@@ -25,6 +25,7 @@ on:
- optimism_sepolia - optimism_sepolia
- polygon - polygon
- rootstock - rootstock
- scroll_sepolia
- shibarium - shibarium
- stability - stability
- zkevm - zkevm
......
...@@ -26,6 +26,7 @@ on: ...@@ -26,6 +26,7 @@ on:
- polygon - polygon
- rootstock - rootstock
- shibarium - shibarium
- scroll_sepolia
- stability - stability
- zkevm - zkevm
- zilliqa_prototestnet - zilliqa_prototestnet
......
# Set of ENVs for Scroll Sepolia Testnet network explorer
# https://scroll-sepolia.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=scroll_sepolia"
# 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_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=scroll-sepolia.blockscout.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/scroll-testnet.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xa0d22caf6217a488b1e97b646c5ed88e8a3020a607bcd1f3fe8d4c430bb19ad5
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(255, 238, 218, 1)'],'text_color':['rgba(25, 6, 2, 1)']}
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/scroll.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/scroll-dark.svg
NEXT_PUBLIC_NETWORK_ID=534351
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/scroll.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/scroll-dark.svg
NEXT_PUBLIC_NETWORK_NAME=Scroll Sepolia Testnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia-rpc.scroll.io
NEXT_PUBLIC_NETWORK_SHORT_NAME=Scroll Sepolia Testnet
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/scroll-testnet.png
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=6Ld0iT8aAAAAAJdju0CmAwGjW7JTDvIw-Q5pwt5T
NEXT_PUBLIC_STATS_API_HOST=https://stats-scroll-sepolia.k8s-prod-2.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE=scroll
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/
\ No newline at end of file
...@@ -436,7 +436,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -436,7 +436,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' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ | | NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' \| 'scroll'` | 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 (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ | | NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required 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 (Optimistic stack only) | - | - | `true` | v1.31.0+ | | NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
......
...@@ -105,6 +105,13 @@ import type { ...@@ -105,6 +105,13 @@ import type {
RewardsUserDailyClaimResponse, RewardsUserDailyClaimResponse,
RewardsUserReferralsResponse, RewardsUserReferralsResponse,
} from 'types/api/rewards'; } from 'types/api/rewards';
import type {
ScrollL2BatchesResponse,
ScrollL2TxnBatch,
ScrollL2TxnBatchTxs,
ScrollL2TxnBatchBlocks,
ScrollL2MessagesResponse,
} from 'types/api/scrollL2';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium';
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
...@@ -983,6 +990,51 @@ export const RESOURCES = { ...@@ -983,6 +990,51 @@ export const RESOURCES = {
path: '/api/v2/shibarium/withdrawals/count', path: '/api/v2/shibarium/withdrawals/count',
}, },
// SCROLL L2
scroll_l2_deposits: {
path: '/api/v2/scroll/deposits',
filterFields: [],
},
scroll_l2_deposits_count: {
path: '/api/v2/scroll/deposits/count',
},
scroll_l2_withdrawals: {
path: '/api/v2/scroll/withdrawals',
filterFields: [],
},
scroll_l2_withdrawals_count: {
path: '/api/v2/scroll/withdrawals/count',
},
scroll_l2_txn_batches: {
path: '/api/v2/scroll/batches',
filterFields: [],
},
scroll_l2_txn_batches_count: {
path: '/api/v2/scroll/batches/count',
},
scroll_l2_txn_batch: {
path: '/api/v2/scroll/batches/:number',
pathParams: [ 'number' as const ],
},
scroll_l2_txn_batch_txs: {
path: '/api/v2/transactions/scroll-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
scroll_l2_txn_batch_blocks: {
path: '/api/v2/blocks/scroll-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
// NOVES-FI // NOVES-FI
noves_transaction: { noves_transaction: {
path: '/api/v2/proxy/noves-fi/transactions/:hash', path: '/api/v2/proxy/noves-fi/transactions/:hash',
...@@ -1130,7 +1182,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1130,7 +1182,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' | 'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' |
'token_transfers_all'; 'token_transfers_all' | 'scroll_l2_txn_batches' | 'scroll_l2_txn_batch_txs' | 'scroll_l2_txn_batch_blocks' |
'scroll_l2_deposits' | 'scroll_l2_withdrawals';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -1278,6 +1331,8 @@ Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse : ...@@ -1278,6 +1331,8 @@ Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse :
Q extends 'zksync_l2_txn_batches_count' ? number : Q extends 'zksync_l2_txn_batches_count' ? number :
Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch : Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch :
Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs : Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs :
Q extends 'scroll_l2_txn_batch_txs' ? ScrollL2TxnBatchTxs :
Q extends 'scroll_l2_txn_batch_blocks' ? ScrollL2TxnBatchBlocks :
Q extends 'contract_security_audits' ? SmartContractSecurityAudits : Q extends 'contract_security_audits' ? SmartContractSecurityAudits :
Q extends 'addresses_lookup' ? bens.LookupAddressResponse : Q extends 'addresses_lookup' ? bens.LookupAddressResponse :
Q extends 'address_domain' ? bens.GetAddressResponse : Q extends 'address_domain' ? bens.GetAddressResponse :
...@@ -1313,6 +1368,13 @@ Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse : ...@@ -1313,6 +1368,13 @@ Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse :
Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse : Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse :
Q extends 'token_transfers_all' ? TokenTransferResponse : Q extends 'token_transfers_all' ? TokenTransferResponse :
Q extends 'address_xstar_score' ? AddressXStarResponse : Q extends 'address_xstar_score' ? AddressXStarResponse :
Q extends 'scroll_l2_txn_batches' ? ScrollL2BatchesResponse :
Q extends 'scroll_l2_txn_batches_count' ? number :
Q extends 'scroll_l2_txn_batch' ? ScrollL2TxnBatch :
Q extends 'scroll_l2_deposits' ? ScrollL2MessagesResponse :
Q extends 'scroll_l2_deposits_count' ? number :
Q extends 'scroll_l2_withdrawals' ? ScrollL2MessagesResponse :
Q extends 'scroll_l2_withdrawals_count' ? number :
never; never;
/* eslint-enable @stylistic/indent */ /* eslint-enable @stylistic/indent */
......
...@@ -109,7 +109,12 @@ export default function useNavItems(): ReturnType { ...@@ -109,7 +109,12 @@ export default function useNavItems(): ReturnType {
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum' || rollupFeature.type === 'zkEvm')) { if (rollupFeature.isEnabled && (
rollupFeature.type === 'optimistic' ||
rollupFeature.type === 'arbitrum' ||
rollupFeature.type === 'zkEvm' ||
rollupFeature.type === 'scroll'
)) {
blockchainNavItems = [ blockchainNavItems = [
[ [
txs, txs,
......
import type { ScrollL2MessagesResponse } from 'types/api/scrollL2';
export const baseResponse: ScrollL2MessagesResponse = {
items: [
{
id: 930795,
origination_transaction_block_number: 20639178,
origination_transaction_hash: '0x70380f2c6ecd53aa6e0608e6c9d770acaa29c0508869ec296bae3e09678ea9f4',
origination_timestamp: '2024-08-30T05:03:23.000000Z',
completion_transaction_hash: null,
value: '5084131319054877748',
},
{
id: 930748,
origination_transaction_block_number: 20638104,
origination_transaction_hash: '0x7e7b4d5ff0b7a6af5e52f4aa2ad9eca3c0c5664368cbb781e04b5b13c6109b2b',
origination_timestamp: '2024-08-30T01:26:35.000000Z',
completion_transaction_hash: '0x426b16ea3a42228f6d754ae55c348986122cdb1e4331b6fd454975776f513ea1',
value: '0',
},
],
next_page_params: {
items_count: 50,
id: 1,
},
};
import type { ScrollL2BatchesResponse } from 'types/api/scrollL2';
export const batchData = {
number: 66928,
commitment_transaction: {
block_number: 19114878,
hash: '0x57552c0dbcf56383ee2efdf8fd6be143b355135fc300361924582c308877b8b7',
timestamp: '2024-01-29T21:31:35.000000Z',
},
confirmation_transaction: {
block_number: null,
hash: null,
timestamp: null,
},
data_availability: {
batch_data_container: 'in_blob4844' as const,
},
start_block: 456000,
end_block: 789000,
transaction_count: 654,
};
export const baseResponse: ScrollL2BatchesResponse = {
items: [
batchData,
{
number: 66879,
commitment_transaction: {
block_number: 19114386,
hash: '0x0d33245814b9e61c8f0ed6fd3fb7464f34be33d2c3aee69629d65e8995d77edc',
timestamp: '2024-01-29T19:52:35.000000Z',
},
confirmation_transaction: {
block_number: 19114558,
hash: '0x6f9a19d503947ec91d6e9d5c2129913a7def86fd0f87061c06e5994cf857bee0',
timestamp: '2024-01-29T20:27:11.000000Z',
},
data_availability: {
batch_data_container: 'in_calldata',
},
start_block: 456000,
end_block: 789000,
transaction_count: 962,
},
],
next_page_params: {
items_count: 50,
number: 1,
},
};
...@@ -67,7 +67,7 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => { ...@@ -67,7 +67,7 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => {
return account(context); return account(context);
}; };
const DEPOSITS_ROLLUP_TYPES: Array<RollupType> = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ]; const DEPOSITS_ROLLUP_TYPES: Array<RollupType> = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum', 'scroll' ];
export const deposits: GetServerSideProps<Props> = async(context) => { export const deposits: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && DEPOSITS_ROLLUP_TYPES.includes(rollupFeature.type))) { if (!(rollupFeature.isEnabled && DEPOSITS_ROLLUP_TYPES.includes(rollupFeature.type))) {
return { return {
...@@ -78,7 +78,7 @@ export const deposits: GetServerSideProps<Props> = async(context) => { ...@@ -78,7 +78,7 @@ export const deposits: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
const WITHDRAWALS_ROLLUP_TYPES: Array<RollupType> = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ]; const WITHDRAWALS_ROLLUP_TYPES: Array<RollupType> = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum', 'scroll' ];
export const withdrawals: GetServerSideProps<Props> = async(context) => { export const withdrawals: GetServerSideProps<Props> = async(context) => {
if ( if (
!config.features.beaconChain.isEnabled && !config.features.beaconChain.isEnabled &&
...@@ -112,7 +112,7 @@ export const outputRoots: GetServerSideProps<Props> = async(context) => { ...@@ -112,7 +112,7 @@ export const outputRoots: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic' ]; const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic', 'scroll' ];
export const batch: GetServerSideProps<Props> = async(context) => { export const batch: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) { if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) {
return { return {
......
...@@ -23,6 +23,8 @@ const Batch = dynamic(() => { ...@@ -23,6 +23,8 @@ const Batch = dynamic(() => {
return import('ui/pages/ZkEvmL2TxnBatch'); return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync': case 'zkSync':
return import('ui/pages/ZkSyncL2TxnBatch'); return import('ui/pages/ZkSyncL2TxnBatch');
case 'scroll':
return import('ui/pages/ScrollL2TxnBatch');
} }
throw new Error('Txn batches feature is not enabled.'); throw new Error('Txn batches feature is not enabled.');
}, { ssr: false }); }, { ssr: false });
......
...@@ -21,6 +21,8 @@ const Batches = dynamic(() => { ...@@ -21,6 +21,8 @@ const Batches = dynamic(() => {
return import('ui/pages/OptimisticL2TxnBatches'); return import('ui/pages/OptimisticL2TxnBatches');
case 'arbitrum': case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatches'); return import('ui/pages/ArbitrumL2TxnBatches');
case 'scroll':
return import('ui/pages/ScrollL2TxnBatches');
} }
throw new Error('Txn batches feature is not enabled.'); throw new Error('Txn batches feature is not enabled.');
}, { ssr: false }); }, { ssr: false });
......
...@@ -24,6 +24,10 @@ const Deposits = dynamic(() => { ...@@ -24,6 +24,10 @@ const Deposits = dynamic(() => {
return import('ui/pages/ZkEvmL2Deposits'); return import('ui/pages/ZkEvmL2Deposits');
} }
if (rollupFeature.isEnabled && rollupFeature.type === 'scroll') {
return import('ui/pages/ScrollL2Deposits');
}
throw new Error('Deposits feature is not enabled.'); throw new Error('Deposits feature is not enabled.');
}, { ssr: false }); }, { ssr: false });
......
...@@ -25,6 +25,10 @@ const Withdrawals = dynamic(() => { ...@@ -25,6 +25,10 @@ const Withdrawals = dynamic(() => {
return import('ui/pages/ZkEvmL2Withdrawals'); return import('ui/pages/ZkEvmL2Withdrawals');
} }
if (rollupFeature.isEnabled && rollupFeature.type === 'scroll') {
return import('ui/pages/ScrollL2Withdrawals');
}
if (beaconChainFeature.isEnabled) { if (beaconChainFeature.isEnabled) {
return import('ui/pages/BeaconChainWithdrawals'); return import('ui/pages/BeaconChainWithdrawals');
} }
......
...@@ -40,6 +40,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -40,6 +40,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ], [ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
[ 'NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS', 'none' ], [ 'NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS', 'none' ],
], ],
scrollRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'scroll' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
],
bridgedTokens: [ bridgedTokens: [
[ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ], [ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ],
[ 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]' ], [ 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]' ],
......
import type { ScrollL2MessageItem, ScrollL2TxnBatch } from 'types/api/scrollL2';
import { TX_HASH } from './tx';
export const SCROLL_L2_TXN_BATCH: ScrollL2TxnBatch = {
commitment_transaction: {
block_number: 4053979,
hash: '0xd04d626495ef69abd37ae3ea585ed03319a3d3b50cf10874f7f36741c7b45a18',
timestamp: '2023-08-09T08:09:12.000000Z',
},
confirmation_transaction: {
block_number: null,
hash: null,
timestamp: null,
},
end_block: 1711,
number: 273,
start_block: 1697,
transaction_count: 15,
data_availability: {
batch_data_container: 'in_blob4844',
},
};
export const SCROLL_L2_MESSAGE_ITEM: ScrollL2MessageItem = {
id: 930795,
origination_transaction_block_number: 20639178,
origination_transaction_hash: TX_HASH,
origination_timestamp: '2024-08-30T05:03:23.000000Z',
completion_transaction_hash: 'TX_HASH',
value: '5084131319054877748',
};
...@@ -18,6 +18,7 @@ const PRESETS = { ...@@ -18,6 +18,7 @@ const PRESETS = {
optimism_sepolia: 'https://optimism-sepolia.blockscout.com', optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
polygon: 'https://polygon.blockscout.com', polygon: 'https://polygon.blockscout.com',
rootstock_testnet: 'https://rootstock-testnet.blockscout.com', rootstock_testnet: 'https://rootstock-testnet.blockscout.com',
scroll_sepolia: 'https://scroll-sepolia.blockscout.com',
shibarium: 'https://www.shibariumscan.io', shibarium: 'https://www.shibariumscan.io',
stability_testnet: 'https://stability-testnet.blockscout.com', stability_testnet: 'https://stability-testnet.blockscout.com',
zkevm: 'https://zkevm.blockscout.com', zkevm: 'https://zkevm.blockscout.com',
......
import type { Block } from './block';
import type { Transaction } from './transaction';
export interface ScrollL2BatchesResponse {
items: Array<ScrollL2TxnBatch>;
next_page_params: {
items_count: number;
number: number;
};
}
type ScrollL2TxnBatchCommitmentTransaction = {
block_number: number;
hash: string;
timestamp: string;
};
type ScrollL2TxnBatchConfirmationTransaction = {
block_number: number | null;
hash: string | null;
timestamp: string | null;
};
export type ScrollL2TxnBatch = {
number: number;
commitment_transaction: ScrollL2TxnBatchCommitmentTransaction;
confirmation_transaction: ScrollL2TxnBatchConfirmationTransaction;
start_block: number;
end_block: number;
transaction_count: number;
data_availability: {
batch_data_container: 'in_blob4844' | 'in_calldata';
};
};
export type ScrollL2TxnBatchTxs = {
items: Array<Transaction>;
next_page_params: {
batch_number: number;
block_number: number;
index: number;
items_count: number;
} | null;
};
export type ScrollL2TxnBatchBlocks = {
items: Array<Block>;
next_page_params: {
batch_number: number;
block_number: number;
items_count: number;
} | null;
};
export type ScrollL2MessagesResponse = {
items: Array<ScrollL2MessageItem>;
next_page_params: {
id: number;
items_count: number;
} | null;
};
export type ScrollL2MessageItem = {
id: number;
origination_transaction_block_number: number;
origination_transaction_hash: string;
origination_timestamp: string;
completion_transaction_hash: string | null;
value: string;
};
export const SCROLL_L2_BLOCK_STATUSES = [
'Confirmed by Sequencer' as const,
'Committed' as const,
'Finalized' as const,
];
export type ScrollL2BlockStatus = typeof SCROLL_L2_BLOCK_STATUSES[number];
...@@ -5,6 +5,7 @@ import type { DecodedInput } from './decodedInput'; ...@@ -5,6 +5,7 @@ import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { NovesTxTranslation } from './noves'; import type { NovesTxTranslation } from './noves';
import type { OptimisticL2WithdrawalStatus } from './optimisticL2'; import type { OptimisticL2WithdrawalStatus } from './optimisticL2';
import type { ScrollL2BlockStatus } from './scrollL2';
import type { TokenInfo } from './token'; import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction'; import type { TxAction } from './txAction';
...@@ -103,6 +104,7 @@ export type Transaction = { ...@@ -103,6 +104,7 @@ export type Transaction = {
// Noves-fi // Noves-fi
translation?: NovesTxTranslation; translation?: NovesTxTranslation;
arbitrum?: ArbitrumTransactionData; arbitrum?: ArbitrumTransactionData;
scroll?: ScrollTransactionData;
}; };
type ArbitrumTransactionData = { type ArbitrumTransactionData = {
...@@ -190,3 +192,17 @@ export interface TransactionsSorting { ...@@ -190,3 +192,17 @@ export interface TransactionsSorting {
export type TransactionsSortingField = TransactionsSorting['sort']; export type TransactionsSortingField = TransactionsSorting['sort'];
export type TransactionsSortingValue = `${ TransactionsSortingField }-${ TransactionsSorting['order'] }`; export type TransactionsSortingValue = `${ TransactionsSortingField }-${ TransactionsSorting['order'] }`;
export type ScrollTransactionData = {
l1_fee: string;
l2_fee: Fee;
l1_fee_commit_scalar: number;
l1_base_fee: number;
l1_blob_base_fee: number;
l1_fee_scalar: number;
l1_fee_overhead: number;
l1_fee_blob_scalar: number;
l1_gas_used: number;
l2_block_status: ScrollL2BlockStatus;
queue_index: number;
};
...@@ -6,6 +6,7 @@ export const ROLLUP_TYPES = [ ...@@ -6,6 +6,7 @@ export const ROLLUP_TYPES = [
'shibarium', 'shibarium',
'zkEvm', 'zkEvm',
'zkSync', 'zkSync',
'scroll',
] as const; ] as const;
export type RollupType = ArrayElement<typeof ROLLUP_TYPES>; export type RollupType = ArrayElement<typeof ROLLUP_TYPES>;
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2MessageItem } from 'types/api/scrollL2';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
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 TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ScrollL2MessageItem; isLoading?: boolean };
const ScrollL2DepositsListItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'scroll') {
return null;
}
const { valueStr } = getCurrencyValue({ value: item.value, decimals: String(config.chain.currency.decimals) });
return (
<ListItemMobileGrid.Container>
<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>
<ListItemMobileGrid.Label isLoading={ isLoading }>Index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.id }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntityL1
isLoading={ isLoading }
hash={ item.origination_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.origination_timestamp }
isLoading={ isLoading }
display="inline-block"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.completion_transaction_hash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.completion_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : (
<chakra.span>
Pending Claim
</chakra.span>
) }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ `${ valueStr } ${ config.chain.currency.symbol }` }
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ScrollL2DepositsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2MessageItem } from 'types/api/scrollL2';
import config from 'configs/app';
import { default as Thead } from 'ui/shared/TheadSticky';
import ScrollL2DepositsTableItem from './ScrollL2DepositsTableItem';
type Props = {
items: Array<ScrollL2MessageItem>;
top: number;
isLoading?: boolean;
};
const ScrollL2DepositsTable = ({ items, top, isLoading }: Props) => {
return (
<Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L1 block</Th>
<Th>Index</Th>
<Th>L1 txn hash</Th>
<Th>Age</Th>
<Th>L2 txn hash</Th>
<Th isNumeric>Value { config.chain.currency.symbol }</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ScrollL2DepositsTableItem key={ String(item.id) + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default ScrollL2DepositsTable;
import { Td, Tr, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2MessageItem } from 'types/api/scrollL2';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
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 TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ScrollL2MessageItem; isLoading?: boolean };
const ScrollL2DepositsTableItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'scroll') {
return null;
}
const { valueStr } = getCurrencyValue({ value: item.value, decimals: String(config.chain.currency.decimals) });
return (
<Tr>
<Td verticalAlign="middle">
<BlockEntityL1
number={ item.origination_transaction_block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>
<span>{ item.id }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<TxEntityL1
isLoading={ isLoading }
hash={ item.origination_transaction_hash }
truncation="constant_long"
noIcon
fontSize="sm"
lineHeight={ 5 }
/>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<TimeAgoWithTooltip
timestamp={ item.origination_timestamp }
isLoading={ isLoading }
color="text_secondary"
/>
</Td>
<Td verticalAlign="middle">
{ item.completion_transaction_hash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.completion_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
) : (
<chakra.span color="text_secondary">
Pending Claim
</chakra.span>
) }
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>{ valueStr }</span>
</Skeleton>
</Td>
</Tr>
);
};
export default ScrollL2DepositsTableItem;
import React from 'react';
import * as messagesMock from 'mocks/scroll/messages';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib';
import ScrollL2Deposits from './ScrollL2Deposits';
test('base view', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.scrollRollup);
await mockApiResponse('scroll_l2_deposits', messagesMock.baseResponse);
await mockApiResponse('scroll_l2_deposits_count', 3971111);
const component = await render(<ScrollL2Deposits/>);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.scrollRollup);
await mockApiResponse('scroll_l2_deposits', messagesMock.baseResponse);
await mockApiResponse('scroll_l2_deposits_count', 3971111);
const component = await render(<ScrollL2Deposits/>);
await expect(component).toHaveScreenshot();
});
});
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 { SCROLL_L2_MESSAGE_ITEM } from 'stubs/scrollL2';
import { generateListStub } from 'stubs/utils';
import ScrollL2DepositsListItem from 'ui/deposits/scrollL2/ScrollL2DepositsListItem';
import ScrollL2DepositsTable from 'ui/deposits/scrollL2/ScrollL2DepositsTable';
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';
const ScrollL2Deposits = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'scroll_l2_deposits',
options: {
placeholderData: generateListStub<'scroll_l2_deposits'>(
SCROLL_L2_MESSAGE_ITEM,
50,
{ next_page_params: { items_count: 50, id: 1 } },
),
},
});
const countersQuery = useApiQuery('scroll_l2_deposits_count', {
queryOptions: {
placeholderData: 1927029,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ScrollL2DepositsListItem
key={ String(item.id) + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ScrollL2DepositsTable items={ data.items } 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() } deposits found
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title={ `Deposits (L1${ nbsp }${ rightLineArrow }${ nbsp }L2)` } withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no deposits."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ScrollL2Deposits;
import React from 'react';
import { batchData } from 'mocks/scroll/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib';
import ScrollL2TxnBatch from './ScrollL2TxnBatch';
const batchNumber = '5';
const hooksConfig = {
router: {
query: { number: batchNumber },
},
};
test.beforeEach(async({ mockTextAd, mockEnvs }) => {
await mockEnvs(ENVS_MAP.scrollRollup);
await mockTextAd();
});
test('base view', async({ render, mockApiResponse }) => {
await mockApiResponse('scroll_l2_txn_batch', batchData, { pathParams: { number: batchNumber } });
const component = await render(<ScrollL2TxnBatch/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, mockApiResponse }) => {
await mockApiResponse('scroll_l2_txn_batch', batchData, { pathParams: { number: batchNumber } });
const component = await render(<ScrollL2TxnBatch/>, { 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 { BLOCK } from 'stubs/block';
import { SCROLL_L2_TXN_BATCH } from 'stubs/scrollL2';
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 ScrollL2TxnBatchDetails from 'ui/txnBatches/scrollL2/ScrollL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const TABS_HEIGHT = 80;
const ScrollL2TxnBatch = () => {
const router = useRouter();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
const tab = getQueryParamString(router.query.tab);
const isMobile = useIsMobile();
const batchQuery = useApiQuery('scroll_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: SCROLL_L2_TXN_BATCH,
},
});
const batchTxsQuery = useQueryWithPages({
resourceName: 'scroll_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'txs'),
placeholderData: generateListStub<'scroll_l2_txn_batch_txs'>(TX, 50, { next_page_params: {
batch_number: 8122,
block_number: 1338932,
index: 0,
items_count: 50,
} }),
},
});
const batchBlocksQuery = useQueryWithPages({
resourceName: 'scroll_l2_txn_batch_blocks',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'blocks'),
placeholderData: generateListStub<'scroll_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: <ScrollL2TxnBatchDetails 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 txn batches list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `Txn 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 ScrollL2TxnBatch;
import React from 'react';
import * as scrollTxnBatchesMock from 'mocks/scroll/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib';
import ScrollL2TxnBatches from './ScrollL2TxnBatches';
test('base view', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => {
test.slow();
await mockEnvs(ENVS_MAP.scrollRollup);
await mockTextAd();
await mockApiResponse('scroll_l2_txn_batches', scrollTxnBatchesMock.baseResponse);
await mockApiResponse('scroll_l2_txn_batches_count', 9927);
const component = await render(<ScrollL2TxnBatches/>);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => {
test.slow();
await mockEnvs(ENVS_MAP.scrollRollup);
await mockTextAd();
await mockApiResponse('scroll_l2_txn_batches', scrollTxnBatchesMock.baseResponse);
await mockApiResponse('scroll_l2_txn_batches_count', 9927);
const component = await render(<ScrollL2TxnBatches/>);
await expect(component).toHaveScreenshot();
});
});
import { Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { SCROLL_L2_TXN_BATCH } from 'stubs/scrollL2';
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 ScrollL2TxnBatchesListItem from 'ui/txnBatches/scrollL2/ScrollL2TxnBatchesListItem';
import ScrollL2TxnBatchesTable from 'ui/txnBatches/scrollL2/ScrollL2TxnBatchesTable';
const ScrollL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'scroll_l2_txn_batches',
options: {
placeholderData: generateListStub<'scroll_l2_txn_batches'>(
SCROLL_L2_TXN_BATCH,
50,
{
next_page_params: {
items_count: 50,
number: 224,
},
},
),
},
});
const countersQuery = useApiQuery('scroll_l2_txn_batches_count', {
queryOptions: {
placeholderData: 123456,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ScrollL2TxnBatchesListItem
key={ item.number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ScrollL2TxnBatchesTable 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">
Txn batch
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text>
(total of { countersQuery.data?.toLocaleString() } batches)
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title="Txn batches" withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no txn batches."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ScrollL2TxnBatches;
import React from 'react';
import * as messagesMock from 'mocks/scroll/messages';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib';
import ScrollL2Withdrawals from './ScrollL2Withdrawals';
test('base view', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.scrollRollup);
await mockApiResponse('scroll_l2_withdrawals', messagesMock.baseResponse);
await mockApiResponse('scroll_l2_withdrawals_count', 3971111);
const component = await render(<ScrollL2Withdrawals/>);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.scrollRollup);
await mockApiResponse('scroll_l2_withdrawals', messagesMock.baseResponse);
await mockApiResponse('scroll_l2_withdrawals_count', 3971111);
const component = await render(<ScrollL2Withdrawals/>);
await expect(component).toHaveScreenshot();
});
});
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 { SCROLL_L2_MESSAGE_ITEM } from 'stubs/scrollL2';
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 ScrollL2WithdrawalsListItem from 'ui/withdrawals/scrollL2/ScrollL2WithdrawalsListItem';
import ScrollL2WithdrawalsTable from 'ui/withdrawals/scrollL2/ScrollL2WithdrawalsTable';
const ScrollL2Withdrawals = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'scroll_l2_withdrawals',
options: {
placeholderData: generateListStub<'scroll_l2_withdrawals'>(
SCROLL_L2_MESSAGE_ITEM,
50,
{ next_page_params: { items_count: 50, id: 1 } },
),
},
});
const countersQuery = useApiQuery('scroll_l2_withdrawals_count', {
queryOptions: {
placeholderData: 1927029,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ScrollL2WithdrawalsListItem
key={ String(item.id) + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ScrollL2WithdrawalsTable items={ data.items } 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() } withdrawals found
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title={ `Withdrawals (L2${ nbsp }${ rightLineArrow }${ nbsp }L1)` } withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no withdrawals."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ScrollL2Withdrawals;
import React from 'react';
import type { ScrollL2TxnBatch } from 'types/api/scrollL2';
import type { ExcludeUndefined } from 'types/utils';
import Tag from 'ui/shared/chakra/Tag';
export interface Props {
container: ExcludeUndefined<ScrollL2TxnBatch['data_availability']['batch_data_container']>;
isLoading?: boolean;
}
const ScrollL2TxnBatchDA = ({ container, isLoading }: Props) => {
const text = (() => {
switch (container) {
case 'in_blob4844':
return 'EIP-4844 blob';
case 'in_calldata':
return 'Calldata';
}
})();
return (
<Tag colorScheme="yellow" isLoading={ isLoading }>
{ text }
</Tag>
);
};
export default React.memo(ScrollL2TxnBatchDA);
import React from 'react';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
export interface Props {
status: 'Finalized' | 'Committed';
isLoading?: boolean;
}
const ZkEvmL2TxnBatchStatus = ({ 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 ZkEvmL2TxnBatchStatus;
...@@ -6,9 +6,9 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -6,9 +6,9 @@ import type { Transaction } from 'types/api/transaction';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
type Props = Pick<Transaction, 'nonce' | 'type' | 'position'>; type Props = Pick<Transaction, 'nonce' | 'type' | 'position'> & { queueIndex?: number };
const TxDetailsOther = ({ nonce, type, position }: Props) => { const TxDetailsOther = ({ nonce, type, position, queueIndex }: Props) => {
return ( return (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
...@@ -27,10 +27,17 @@ const TxDetailsOther = ({ nonce, type, position }: Props) => { ...@@ -27,10 +27,17 @@ const TxDetailsOther = ({ nonce, type, position }: Props) => {
{ type === 3 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-4844)</Text> } { type === 3 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-4844)</Text> }
</Box> </Box>
), ),
queueIndex !== undefined ? (
<Box key="queueIndex">
<Text as="span" fontWeight="500">Queue index: </Text>
<Text fontWeight="600" as="span">{ queueIndex }</Text>
</Box>
) : (
<Box key="nonce"> <Box key="nonce">
<Text as="span" fontWeight="500">Nonce: </Text> <Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ nonce }</Text> <Text fontWeight="600" as="span">{ nonce }</Text>
</Box>, </Box>
),
position !== null && position !== undefined && ( position !== null && position !== undefined && (
<Box key="position"> <Box key="position">
<Text as="span" fontWeight="500">Position: </Text> <Text as="span" fontWeight="500">Position: </Text>
......
...@@ -16,6 +16,7 @@ import BigNumber from 'bignumber.js'; ...@@ -16,6 +16,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 { SCROLL_L2_BLOCK_STATUSES } from 'types/api/scrollL2';
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';
...@@ -63,6 +64,8 @@ import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers'; ...@@ -63,6 +64,8 @@ import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo'; import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
import TxInfoScrollFees from './TxInfoScrollFees';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
interface Props { interface Props {
...@@ -166,7 +169,8 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -166,7 +169,8 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
isLoading={ isLoading } isLoading={ isLoading }
> >
{ {
rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum') ? rollupFeature.isEnabled &&
(rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum' || rollupFeature.type === 'scroll') ?
'L2 status and method' : 'L2 status and method' :
'Status and method' 'Status and method'
} }
...@@ -297,6 +301,12 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -297,6 +301,12 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</Skeleton> </Skeleton>
</> </>
) } ) }
{ data.scroll?.l2_block_status && (
<>
<TextSeparator color="gray.500"/>
<VerificationSteps steps={ SCROLL_L2_BLOCK_STATUSES } currentStep={ data.scroll.l2_block_status } isLoading={ isLoading }/>
</>
) }
</DetailsInfoItem.Value> </DetailsInfoItem.Value>
{ data.zkevm_batch_number && !config.UI.views.tx.hiddenFields?.batch && ( { data.zkevm_batch_number && !config.UI.views.tx.hiddenFields?.batch && (
...@@ -661,6 +671,20 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -661,6 +671,20 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</> </>
) } ) }
{ data.scroll?.l1_gas_used !== undefined && (
<>
<DetailsInfoItem.Label
hint="Total gas used on L1"
isLoading={ isLoading }
>
L1 Gas used
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isLoading }>{ BigNumber(data.scroll?.l1_gas_used || 0).toFormat() }</Skeleton>
</DetailsInfoItem.Value>
</>
) }
{ !config.UI.views.tx.hiddenFields?.gas_fees && { !config.UI.views.tx.hiddenFields?.gas_fees &&
(data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && ( (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && (
<> <>
...@@ -767,6 +791,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -767,6 +791,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
) } ) }
</> </>
) } ) }
<TxInfoScrollFees data={ data } isLoading={ isLoading }/>
<GridItem colSpan={{ base: undefined, lg: 2 }}> <GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="TxInfo__cutLink"> <Element name="TxInfo__cutLink">
...@@ -870,7 +895,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -870,7 +895,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</> </>
) } ) }
<TxDetailsOther nonce={ data.nonce } type={ data.type } position={ data.position }/> <TxDetailsOther nonce={ data.nonce } type={ data.type } position={ data.position } queueIndex={ data.scroll?.queue_index }/>
<DetailsInfoItem.Label <DetailsInfoItem.Label
hint="Binary data included with the transaction. See logs tab for additional info" hint="Binary data included with the transaction. See logs tab for additional info"
......
import { Skeleton, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import { WEI_IN_GWEI } from 'lib/consts';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TextSeparator from 'ui/shared/TextSeparator';
type Props = {
data: Transaction;
isLoading: boolean;
};
export const TxInfoScrollFees = ({ data, isLoading }: Props) => {
return (
<>
{ data.scroll?.l1_fee !== undefined && (
<>
<DetailsInfoItem.Label
hint="L1 fee that pays for rollup costs."
isLoading={ isLoading }
>
L1 data fee
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<CurrencyValue
value={ data.scroll?.l1_fee }
currency={ currencyUnits.ether }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem.Value>
</>
) }
{ data.scroll?.l2_fee !== undefined && (
<>
<DetailsInfoItem.Label
hint="L2 execution fee"
isLoading={ isLoading }
>
Execution fee
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<CurrencyValue
value={ data.scroll?.l2_fee.value }
currency={ currencyUnits.ether }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem.Value>
</>
) }
{ data.scroll?.l1_fee_commit_scalar !== undefined && (
<>
<DetailsInfoItem.Label
hint="Commitment scalar"
isLoading={ isLoading }
>
L1 commit scalar
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<CurrencyValue
value={ data.scroll?.l1_fee_commit_scalar }
currency={ currencyUnits.ether }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem.Value>
</>
) }
{ data.scroll?.l1_fee_overhead !== undefined && (
<>
<DetailsInfoItem.Label
hint="Additional gas overhead of a data commitment transaction"
isLoading={ isLoading }
>
L1 Fee Overhead
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isLoading }>
<CurrencyValue
value={ data.scroll?.l1_fee_overhead }
currency={ currencyUnits.ether }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</Skeleton>
</DetailsInfoItem.Value>
</>
) }
{ (data.scroll?.l1_base_fee !== undefined || data.scroll?.l1_fee_scalar !== undefined) && (
<>
<DetailsInfoItem.Label
hint="L1 gas fees"
isLoading={ isLoading }
>
L1 gas fees
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.scroll?.l1_base_fee !== undefined && (
<Skeleton isLoaded={ !isLoading }>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.scroll?.l1_base_fee || 0).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
</Skeleton>
) }
{ data.scroll?.l1_fee_scalar !== undefined && (
<Skeleton isLoaded={ !isLoading }>
<TextSeparator/>
<Text as="span" fontWeight="500">Scalar: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.scroll?.l1_fee_scalar || 0).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
</Skeleton>
) }
</DetailsInfoItem.Value>
</>
) }
{ (data.scroll?.l1_blob_base_fee !== undefined || data.scroll?.l1_fee_blob_scalar !== undefined) && (
<>
<DetailsInfoItem.Label
hint="L1 blob fees"
isLoading={ isLoading }
>
L1 blob fees
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.scroll?.l1_blob_base_fee !== undefined && (
<Skeleton isLoaded={ !isLoading }>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.scroll?.l1_blob_base_fee || 0).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
</Skeleton>
) }
{ data.scroll?.l1_fee_blob_scalar !== undefined && (
<Skeleton isLoaded={ !isLoading }>
<TextSeparator/>
<Text as="span" fontWeight="500">Scalar: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.scroll?.l1_fee_blob_scalar || 0).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
</Skeleton>
) }
</DetailsInfoItem.Value>
</>
) }
</>
);
};
export default TxInfoScrollFees;
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 { ScrollL2TxnBatch } from 'types/api/scrollL2';
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 ScrollL2TxnBatchDA from 'ui/shared/batch/ScrollL2TxnBatchDA';
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 LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import ScrollL2TxnBatchStatus from 'ui/shared/statusTag/ScrollL2TxnBatchStatus';
interface Props {
query: UseQueryResult<ScrollL2TxnBatch, ResourceError>;
}
const ScrollL2TxnBatchDetails = ({ 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"
>
Txn batch number
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.number }
</Skeleton>
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous txn batch"
nextLabel="View next txn batch"
isPrevDisabled={ data.number === 0 }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Data availability container"
>
Container
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<ScrollL2TxnBatchDA container={ data.data_availability.batch_data_container } isLoading={ isPlaceholderData }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Status of this batch"
>
Status
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<ScrollL2TxnBatchStatus status={ data.confirmation_transaction.hash ? 'Finalized' : 'Committed' } isLoading={ isPlaceholderData }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Date and time at which batch is finalized to L1"
>
Finalized timestamp
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.confirmation_transaction.timestamp ?
<DetailsTimestamp timestamp={ data.confirmation_transaction.timestamp }isLoading={ isPlaceholderData }/> :
<Skeleton isLoaded={ !isPlaceholderData } display="inline-block">Pending</Skeleton>
}
</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.transaction_count.toLocaleString() } transaction{ data.transaction_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.toLocaleString() } block{ blocksCount === 1 ? '' : 's' }
</LinkInternal>
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Date and time at which batch is committed to L1"
>
Committed timestamp
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.commitment_transaction.timestamp ?
<DetailsTimestamp timestamp={ data.commitment_transaction.timestamp }isLoading={ isPlaceholderData }/> :
<Skeleton isLoaded={ !isPlaceholderData } display="inline-block">Pending</Skeleton>
}
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Hash of L1 transaction this batch was committed in"
>
Committed transaction hash
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.commitment_transaction.hash }
maxW="100%"
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="L1 block that includes transaction with this batch commitment"
>
Committed block
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<BlockEntityL1
isLoading={ isPlaceholderData }
number={ data.commitment_transaction.block_number }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Hash of L1 transaction this batch was finalized in"
>
Finalized transaction hash
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.confirmation_transaction.hash ? (
<TxEntityL1
isLoading={ isPlaceholderData }
hash={ data.confirmation_transaction.hash }
maxW="100%"
/>
) : <Skeleton isLoaded={ !isPlaceholderData } display="inline-block">Pending</Skeleton> }
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="L1 block that includes transaction with this batch finalization data"
>
Finalized block
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.confirmation_transaction.block_number ? (
<BlockEntityL1
isLoading={ isPlaceholderData }
number={ data.confirmation_transaction.block_number }
/>
) : <Skeleton isLoaded={ !isPlaceholderData } display="inline-block">Pending</Skeleton> }
</DetailsInfoItem.Value>
</Grid>
);
};
export default ScrollL2TxnBatchDetails;
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2TxnBatch } from 'types/api/scrollL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import ScrollL2TxnBatchDA from 'ui/shared/batch/ScrollL2TxnBatchDA';
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 ScrollL2TxnBatchStatus from 'ui/shared/statusTag/ScrollL2TxnBatchStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ScrollL2TxnBatch; isLoading?: boolean };
const ScrollL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'scroll') {
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 }>Data availability</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ScrollL2TxnBatchDA container={ item.data_availability.batch_data_container } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ScrollL2TxnBatchStatus status={ item.confirmation_transaction.hash ? 'Finalized' : 'Committed' } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Committed block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntityL1
number={ item.commitment_transaction.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Committed txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntityL1
hash={ item.commitment_transaction.hash }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.commitment_transaction.timestamp }
fallbackText="Undefined"
isLoading={ isLoading }
display="inline-block"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Finalized block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.confirmation_transaction.block_number ? (
<BlockEntityL1
number={ item.confirmation_transaction.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
) : <Skeleton isLoaded={ !isLoading } display="inline-block">Pending</Skeleton> }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Finalized txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.confirmation_transaction.hash ? (
<TxEntityL1
hash={ item.confirmation_transaction.hash }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
) : <Skeleton isLoaded={ !isLoading } display="inline-block">Pending</Skeleton> }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Blocks count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'blocks' } }) }
isLoading={ isLoading }
fontWeight={ 600 }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ (item.end_block - item.start_block + 1).toLocaleString() }
</Skeleton>
</LinkInternal>
</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.transaction_count.toLocaleString() }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ScrollL2TxnBatchesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2TxnBatch } from 'types/api/scrollL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import ScrollL2TxnBatchesTableItem from './ScrollL2TxnBatchesTableItem';
type Props = {
items: Array<ScrollL2TxnBatch>;
top: number;
isLoading?: boolean;
};
const ScrollL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Batch #</Th>
<Th>Container</Th>
<Th>Status</Th>
<Th>Committed block</Th>
<Th>Committed txn hash</Th>
<Th>Age</Th>
<Th>Finalized block</Th>
<Th>Finalized txn hash</Th>
<Th isNumeric>Blocks</Th>
<Th isNumeric>Txn</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ScrollL2TxnBatchesTableItem
key={ item.number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default ScrollL2TxnBatchesTable;
import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2TxnBatch } from 'types/api/scrollL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import ScrollL2TxnBatchDA from 'ui/shared/batch/ScrollL2TxnBatchDA';
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 ScrollL2TxnBatchStatus from 'ui/shared/statusTag/ScrollL2TxnBatchStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ScrollL2TxnBatch; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'scroll') {
return null;
}
return (
<Tr>
<Td verticalAlign="middle">
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
<ScrollL2TxnBatchDA container={ item.data_availability?.batch_data_container } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<ScrollL2TxnBatchStatus status={ item.confirmation_transaction.hash ? 'Finalized' : 'Committed' } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<BlockEntityL1
number={ item.commitment_transaction.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
</Td>
<Td verticalAlign="middle">
<TxEntityL1
hash={ item.commitment_transaction.hash }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</Td>
<Td verticalAlign="middle">
<TimeAgoWithTooltip
timestamp={ item.commitment_transaction.timestamp }
fallbackText="Undefined"
isLoading={ isLoading }
color="text_secondary"
/>
</Td>
<Td verticalAlign="middle">
{ item.confirmation_transaction.block_number ? (
<BlockEntityL1
number={ item.confirmation_transaction.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
/>
) : <Skeleton isLoaded={ !isLoading } display="inline-block">Pending</Skeleton> }
</Td>
<Td verticalAlign="middle">
{ item.confirmation_transaction.hash ? (
<TxEntityL1
hash={ item.confirmation_transaction.hash }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : <Skeleton isLoaded={ !isLoading } display="inline-block">Pending</Skeleton> }
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'blocks' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
{ (item.end_block - item.start_block + 1).toLocaleString() }
</Skeleton>
</LinkInternal>
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
{ item.transaction_count.toLocaleString() }
</Skeleton>
</LinkInternal>
</Td>
</Tr>
);
};
export default TxnBatchesTableItem;
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2MessageItem } from 'types/api/scrollL2';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ScrollL2MessageItem; isLoading?: boolean };
const ScrollL2WithdrawalsListItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'scroll') {
return null;
}
const { valueStr } = getCurrencyValue({ value: item.value, decimals: String(config.chain.currency.decimals) });
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity
number={ item.origination_transaction_block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.id }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntity
isLoading={ isLoading }
hash={ item.origination_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.origination_timestamp }
isLoading={ isLoading }
display="inline-block"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.completion_transaction_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.completion_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : (
<chakra.span>
Pending Claim
</chakra.span>
) }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ `${ valueStr } ${ config.chain.currency.symbol }` }
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ScrollL2WithdrawalsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2MessageItem } from 'types/api/scrollL2';
import config from 'configs/app';
import { default as Thead } from 'ui/shared/TheadSticky';
import ScrollL2WithdrawalsTableItem from './ScrollL2WithdrawalsTableItem';
type Props = {
items: Array<ScrollL2MessageItem>;
top: number;
isLoading?: boolean;
};
const ScrollL2WithdrawalsTable = ({ items, top, isLoading }: Props) => {
return (
<Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L2 block</Th>
<Th>Index</Th>
<Th>L2 txn hash</Th>
<Th>Age</Th>
<Th>L1 txn hash</Th>
<Th isNumeric>{ `Value ${ config.chain.currency.symbol }` }</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ScrollL2WithdrawalsTableItem key={ String(item.id) + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default ScrollL2WithdrawalsTable;
import { Td, Tr, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ScrollL2MessageItem } from 'types/api/scrollL2';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ScrollL2MessageItem; isLoading?: boolean };
const ScrollL2WithdrawalsTableItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'scroll') {
return null;
}
const { valueStr } = getCurrencyValue({ value: item.value, decimals: String(config.chain.currency.decimals) });
return (
<Tr>
<Td verticalAlign="middle">
<BlockEntity
number={ item.origination_transaction_block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>
<span>{ item.id }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<TxEntity
isLoading={ isLoading }
hash={ item.origination_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<TimeAgoWithTooltip
timestamp={ item.origination_timestamp }
isLoading={ isLoading }
color="text_secondary"
/>
</Td>
<Td verticalAlign="middle">
{ item.completion_transaction_hash ? (
<TxEntityL1
isLoading={ isLoading }
hash={ item.completion_transaction_hash }
truncation="constant_long"
noIcon
fontSize="sm"
lineHeight={ 5 }
/>
) : (
<chakra.span color="text_secondary">
Pending Claim
</chakra.span>
) }
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>{ valueStr }</span>
</Skeleton>
</Td>
</Tr>
);
};
export default ScrollL2WithdrawalsTableItem;
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