diff --git a/.github/workflows/deploy-review-l2.yml b/.github/workflows/deploy-review-l2.yml index 2ee4dd35e7..2b6956254d 100644 --- a/.github/workflows/deploy-review-l2.yml +++ b/.github/workflows/deploy-review-l2.yml @@ -25,6 +25,7 @@ on: - optimism_sepolia - polygon - rootstock + - scroll_sepolia - shibarium - stability - zkevm diff --git a/.github/workflows/deploy-review.yml b/.github/workflows/deploy-review.yml index f183ead094..e62dd3110c 100644 --- a/.github/workflows/deploy-review.yml +++ b/.github/workflows/deploy-review.yml @@ -26,6 +26,7 @@ on: - polygon - rootstock - shibarium + - scroll_sepolia - stability - zkevm - zksync diff --git a/configs/envs/.env.scroll_sepolia b/configs/envs/.env.scroll_sepolia new file mode 100644 index 0000000000..c29abc0442 --- /dev/null +++ b/configs/envs/.env.scroll_sepolia @@ -0,0 +1,41 @@ +# 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 diff --git a/docs/ENVS.md b/docs/ENVS.md index 4e7c0c8514..5016b7c2f1 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -433,7 +433,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi | 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_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+ | diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 68099b4fc0..031290f516 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -105,6 +105,13 @@ import type { RewardsUserDailyClaimResponse, RewardsUserReferralsResponse, } 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 { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; import type { HomeStats } from 'types/api/stats'; @@ -983,6 +990,51 @@ export const RESOURCES = { 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_transaction: { path: '/api/v2/proxy/noves-fi/transactions/:hash', @@ -1127,7 +1179,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' | '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 = ResourcePayload; @@ -1275,6 +1328,8 @@ Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse : Q extends 'zksync_l2_txn_batches_count' ? number : Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch : 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 'addresses_lookup' ? bens.LookupAddressResponse : Q extends 'address_domain' ? bens.GetAddressResponse : @@ -1310,6 +1365,13 @@ Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse : Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse : Q extends 'token_transfers_all' ? TokenTransferResponse : 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; /* eslint-enable @stylistic/indent */ diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index b3bf63f39f..13fccd17a7 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -109,7 +109,12 @@ export default function useNavItems(): ReturnType { 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 = [ [ txs, diff --git a/mocks/scroll/messages.ts b/mocks/scroll/messages.ts new file mode 100644 index 0000000000..d2bbf43a70 --- /dev/null +++ b/mocks/scroll/messages.ts @@ -0,0 +1,26 @@ +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, + }, +}; diff --git a/mocks/scroll/txnBatches.ts b/mocks/scroll/txnBatches.ts new file mode 100644 index 0000000000..03e038b4fb --- /dev/null +++ b/mocks/scroll/txnBatches.ts @@ -0,0 +1,50 @@ +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, + }, +}; diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index ecf6277d44..4b9f469b36 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -67,7 +67,7 @@ export const verifiedAddresses: GetServerSideProps = async(context) => { return account(context); }; -const DEPOSITS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ]; +const DEPOSITS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum', 'scroll' ]; export const deposits: GetServerSideProps = async(context) => { if (!(rollupFeature.isEnabled && DEPOSITS_ROLLUP_TYPES.includes(rollupFeature.type))) { return { @@ -78,7 +78,7 @@ export const deposits: GetServerSideProps = async(context) => { return base(context); }; -const WITHDRAWALS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ]; +const WITHDRAWALS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum', 'scroll' ]; export const withdrawals: GetServerSideProps = async(context) => { if ( !config.features.beaconChain.isEnabled && @@ -112,7 +112,7 @@ export const optimisticRollup: GetServerSideProps = async(context) => { return base(context); }; -const BATCH_ROLLUP_TYPES: Array = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic' ]; +const BATCH_ROLLUP_TYPES: Array = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic', 'scroll' ]; export const batch: GetServerSideProps = async(context) => { if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) { return { diff --git a/pages/batches/[number].tsx b/pages/batches/[number].tsx index 20b4bf49ca..6239aefafe 100644 --- a/pages/batches/[number].tsx +++ b/pages/batches/[number].tsx @@ -23,6 +23,8 @@ const Batch = dynamic(() => { return import('ui/pages/ZkEvmL2TxnBatch'); case 'zkSync': return import('ui/pages/ZkSyncL2TxnBatch'); + case 'scroll': + return import('ui/pages/ScrollL2TxnBatch'); } throw new Error('Txn batches feature is not enabled.'); }, { ssr: false }); diff --git a/pages/batches/index.tsx b/pages/batches/index.tsx index 374d24763c..176bce9f04 100644 --- a/pages/batches/index.tsx +++ b/pages/batches/index.tsx @@ -21,6 +21,8 @@ const Batches = dynamic(() => { return import('ui/pages/OptimisticL2TxnBatches'); case 'arbitrum': return import('ui/pages/ArbitrumL2TxnBatches'); + case 'scroll': + return import('ui/pages/ScrollL2TxnBatches'); } throw new Error('Txn batches feature is not enabled.'); }, { ssr: false }); diff --git a/pages/deposits/index.tsx b/pages/deposits/index.tsx index 4ebc176013..1d18dd940f 100644 --- a/pages/deposits/index.tsx +++ b/pages/deposits/index.tsx @@ -24,6 +24,10 @@ const Deposits = dynamic(() => { return import('ui/pages/ZkEvmL2Deposits'); } + if (rollupFeature.isEnabled && rollupFeature.type === 'scroll') { + return import('ui/pages/ScrollL2Deposits'); + } + throw new Error('Deposits feature is not enabled.'); }, { ssr: false }); diff --git a/pages/withdrawals/index.tsx b/pages/withdrawals/index.tsx index eca5dd2df7..eb0da1d5df 100644 --- a/pages/withdrawals/index.tsx +++ b/pages/withdrawals/index.tsx @@ -25,6 +25,10 @@ const Withdrawals = dynamic(() => { return import('ui/pages/ZkEvmL2Withdrawals'); } + if (rollupFeature.isEnabled && rollupFeature.type === 'scroll') { + return import('ui/pages/ScrollL2Withdrawals'); + } + if (beaconChainFeature.isEnabled) { return import('ui/pages/BeaconChainWithdrawals'); } diff --git a/playwright/fixtures/mockEnvs.ts b/playwright/fixtures/mockEnvs.ts index 689091e225..c1b01ff1da 100644 --- a/playwright/fixtures/mockEnvs.ts +++ b/playwright/fixtures/mockEnvs.ts @@ -39,6 +39,10 @@ export const ENVS_MAP: Record> = { [ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ], [ 'NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS', 'none' ], ], + scrollRollup: [ + [ 'NEXT_PUBLIC_ROLLUP_TYPE', 'scroll' ], + [ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ], + ], 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_BRIDGES', '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]' ], diff --git a/stubs/scrollL2.ts b/stubs/scrollL2.ts new file mode 100644 index 0000000000..d1a8881154 --- /dev/null +++ b/stubs/scrollL2.ts @@ -0,0 +1,32 @@ +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', +}; diff --git a/tools/preset-sync/index.ts b/tools/preset-sync/index.ts index 3c61bfdcbe..51d6f0765a 100755 --- a/tools/preset-sync/index.ts +++ b/tools/preset-sync/index.ts @@ -18,6 +18,7 @@ const PRESETS = { optimism_sepolia: 'https://optimism-sepolia.blockscout.com', polygon: 'https://polygon.blockscout.com', rootstock_testnet: 'https://rootstock-testnet.blockscout.com', + scroll_sepolia: 'https://scroll-sepolia.blockscout.com', shibarium: 'https://www.shibariumscan.io', stability_testnet: 'https://stability-testnet.blockscout.com', zkevm: 'https://zkevm.blockscout.com', diff --git a/types/api/scrollL2.ts b/types/api/scrollL2.ts new file mode 100644 index 0000000000..c66b79889c --- /dev/null +++ b/types/api/scrollL2.ts @@ -0,0 +1,80 @@ +import type { Block } from './block'; +import type { Transaction } from './transaction'; + +export interface ScrollL2BatchesResponse { + items: Array; + 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'; + }; +}; + +// check next_page_params!!! +export type ScrollL2TxnBatchTxs = { + items: Array; + next_page_params: { + batch_number: number; + block_number: number; + index: number; + items_count: number; + } | null; +}; + +export type ScrollL2TxnBatchBlocks = { + items: Array; + next_page_params: { + batch_number: number; + block_number: number; + index: number; + items_count: number; + } | null; +}; + +export type ScrollL2MessagesResponse = { + items: Array; + 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]; diff --git a/types/api/transaction.ts b/types/api/transaction.ts index c024c045e7..3b6c1ff35e 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -5,6 +5,7 @@ import type { DecodedInput } from './decodedInput'; import type { Fee } from './fee'; import type { NovesTxTranslation } from './noves'; import type { OptimisticL2WithdrawalStatus } from './optimisticL2'; +import type { ScrollL2BlockStatus } from './scrollL2'; import type { TokenInfo } from './token'; import type { TokenTransfer } from './tokenTransfer'; import type { TxAction } from './txAction'; @@ -99,6 +100,7 @@ export type Transaction = { // Noves-fi translation?: NovesTxTranslation; arbitrum?: ArbitrumTransactionData; + scroll?: ScrollTransactionData; }; type ArbitrumTransactionData = { @@ -186,3 +188,17 @@ export interface TransactionsSorting { export type TransactionsSortingField = TransactionsSorting['sort']; 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; +}; diff --git a/types/client/rollup.ts b/types/client/rollup.ts index a2775e289b..9e20b89a1a 100644 --- a/types/client/rollup.ts +++ b/types/client/rollup.ts @@ -6,6 +6,7 @@ export const ROLLUP_TYPES = [ 'shibarium', 'zkEvm', 'zkSync', + 'scroll', ] as const; export type RollupType = ArrayElement; diff --git a/ui/deposits/scrollL2/ScrollL2DepositsListItem.tsx b/ui/deposits/scrollL2/ScrollL2DepositsListItem.tsx new file mode 100644 index 0000000000..37a6d20602 --- /dev/null +++ b/ui/deposits/scrollL2/ScrollL2DepositsListItem.tsx @@ -0,0 +1,94 @@ +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 ( + + + L1 block + + + + + Index + + + { item.id } + + + + L1 txn hash + + + + + Age + + + + + L2 txn hash + + { item.completion_transaction_hash ? ( + + ) : ( + + Pending Claim + + ) } + + + Value + + + { `${ valueStr } ${ config.chain.currency.symbol }` } + + + + + ); +}; + +export default ScrollL2DepositsListItem; diff --git a/ui/deposits/scrollL2/ScrollL2DepositsTable.tsx b/ui/deposits/scrollL2/ScrollL2DepositsTable.tsx new file mode 100644 index 0000000000..0a272a7362 --- /dev/null +++ b/ui/deposits/scrollL2/ScrollL2DepositsTable.tsx @@ -0,0 +1,39 @@ +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; + top: number; + isLoading?: boolean; + }; + +const ScrollL2DepositsTable = ({ items, top, isLoading }: Props) => { + return ( + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
L1 blockIndexL1 txn hashAgeL2 txn hashValue { config.chain.currency.symbol }
+ ); +}; + +export default ScrollL2DepositsTable; diff --git a/ui/deposits/scrollL2/ScrollL2DepositsTableItem.tsx b/ui/deposits/scrollL2/ScrollL2DepositsTableItem.tsx new file mode 100644 index 0000000000..4a48813c23 --- /dev/null +++ b/ui/deposits/scrollL2/ScrollL2DepositsTableItem.tsx @@ -0,0 +1,83 @@ +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 ( + + + + + + + { item.id } + + + + + + + + + + { item.completion_transaction_hash ? ( + + ) : ( + + Pending Claim + + ) } + + + + { valueStr } + + + + ); +}; + +export default ScrollL2DepositsTableItem; diff --git a/ui/pages/ScrollL2Deposits.pw.tsx b/ui/pages/ScrollL2Deposits.pw.tsx new file mode 100644 index 0000000000..82f0e00e5d --- /dev/null +++ b/ui/pages/ScrollL2Deposits.pw.tsx @@ -0,0 +1,33 @@ +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(); + + 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(); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/ScrollL2Deposits.tsx b/ui/pages/ScrollL2Deposits.tsx new file mode 100644 index 0000000000..8274e8caf9 --- /dev/null +++ b/ui/pages/ScrollL2Deposits.tsx @@ -0,0 +1,82 @@ +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 ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError) { + return null; + } + + return ( + + A total of { countersQuery.data?.toLocaleString() } deposits found + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ScrollL2Deposits; diff --git a/ui/pages/ScrollL2TxnBatch.pw.tsx b/ui/pages/ScrollL2TxnBatch.pw.tsx new file mode 100644 index 0000000000..c8a4f4f524 --- /dev/null +++ b/ui/pages/ScrollL2TxnBatch.pw.tsx @@ -0,0 +1,35 @@ +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(, { 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(, { hooksConfig }); + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/ScrollL2TxnBatch.tsx b/ui/pages/ScrollL2TxnBatch.tsx new file mode 100644 index 0000000000..d4bc2bb816 --- /dev/null +++ b/ui/pages/ScrollL2TxnBatch.tsx @@ -0,0 +1,137 @@ +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, + index: 0, + } }), + }, + }); + + 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 = React.useMemo(() => ([ + { id: 'index', title: 'Details', component: }, + { + id: 'txs', + title: 'Transactions', + component: , + }, + { + id: 'blocks', + title: 'Blocks', + component: , + }, + ].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 ( + <> + + + { batchQuery.isPlaceholderData ? + : ( + : null } + stickyEnabled={ hasPagination } + /> + ) } + + ); +}; + +export default ScrollL2TxnBatch; diff --git a/ui/pages/ScrollL2TxnBatches.pw.tsx b/ui/pages/ScrollL2TxnBatches.pw.tsx new file mode 100644 index 0000000000..1aa2589c6a --- /dev/null +++ b/ui/pages/ScrollL2TxnBatches.pw.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import * as scrollTxnBatchesMock from 'mocks/scroll/txnBatches'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; + +import ScrollL2TxnBatches from './ScrollL2TxnBatches'; + +test('base view +@mobile', 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(); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/ScrollL2TxnBatches.tsx b/ui/pages/ScrollL2TxnBatches.tsx new file mode 100644 index 0000000000..60cf2f4fb8 --- /dev/null +++ b/ui/pages/ScrollL2TxnBatches.tsx @@ -0,0 +1,86 @@ +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 ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError || isError || !data?.items.length) { + return null; + } + + return ( + + Txn batch + #{ data.items[0].number } to + #{ data.items[data.items.length - 1].number } + (total of { countersQuery.data?.toLocaleString() } batches) + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ScrollL2TxnBatches; diff --git a/ui/pages/ScrollL2Withdrawals.pw.tsx b/ui/pages/ScrollL2Withdrawals.pw.tsx new file mode 100644 index 0000000000..9c1c2e5b06 --- /dev/null +++ b/ui/pages/ScrollL2Withdrawals.pw.tsx @@ -0,0 +1,33 @@ +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(); + + 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(); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/ScrollL2Withdrawals.tsx b/ui/pages/ScrollL2Withdrawals.tsx new file mode 100644 index 0000000000..abba1ff9d2 --- /dev/null +++ b/ui/pages/ScrollL2Withdrawals.tsx @@ -0,0 +1,82 @@ +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 ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError) { + return null; + } + + return ( + + A total of { countersQuery.data?.toLocaleString() } withdrawals found + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ScrollL2Withdrawals; diff --git a/ui/pages/__screenshots__/ScrollL2Deposits.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/ScrollL2Deposits.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..0b796b1b7d Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2Deposits.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2Deposits.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/ScrollL2Deposits.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..dd53695ab6 Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2Deposits.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..74a843a4dd Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..74a843a4dd Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..bbab012f20 Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2TxnBatch.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ScrollL2TxnBatches.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..bffcc9fe97 Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ScrollL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..d086f64128 Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2Withdrawals.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/ScrollL2Withdrawals.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..d52eda465f Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2Withdrawals.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/ScrollL2Withdrawals.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/ScrollL2Withdrawals.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..3e14692aaa Binary files /dev/null and b/ui/pages/__screenshots__/ScrollL2Withdrawals.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/shared/batch/ScrollL2TxnBatchDA.tsx b/ui/shared/batch/ScrollL2TxnBatchDA.tsx new file mode 100644 index 0000000000..5e47a8d290 --- /dev/null +++ b/ui/shared/batch/ScrollL2TxnBatchDA.tsx @@ -0,0 +1,31 @@ +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; + isLoading?: boolean; +} + +const ScrollL2TxnBatchDA = ({ container, isLoading }: Props) => { + + const text = (() => { + switch (container) { + case 'in_blob4844': + return 'EIP-4844 blob'; + case 'in_calldata': + return 'Calldata'; + } + })(); + + return ( + + { text } + + ); +}; + +export default React.memo(ScrollL2TxnBatchDA); diff --git a/ui/shared/statusTag/ScrollL2TxnBatchStatus.tsx b/ui/shared/statusTag/ScrollL2TxnBatchStatus.tsx new file mode 100644 index 0000000000..ca6ffbd8a5 --- /dev/null +++ b/ui/shared/statusTag/ScrollL2TxnBatchStatus.tsx @@ -0,0 +1,26 @@ +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 ; +}; + +export default ZkEvmL2TxnBatchStatus; diff --git a/ui/tx/details/TxDetailsOther.tsx b/ui/tx/details/TxDetailsOther.tsx index 490e190764..e06265867a 100644 --- a/ui/tx/details/TxDetailsOther.tsx +++ b/ui/tx/details/TxDetailsOther.tsx @@ -6,9 +6,9 @@ import type { Transaction } from 'types/api/transaction'; import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import TextSeparator from 'ui/shared/TextSeparator'; -type Props = Pick; +type Props = Pick & { queueIndex?: number }; -const TxDetailsOther = ({ nonce, type, position }: Props) => { +const TxDetailsOther = ({ nonce, type, position, queueIndex }: Props) => { return ( <> { { type === 3 && (EIP-4844) } ), - - Nonce: - { nonce } - , + queueIndex !== undefined ? ( + + Queue index: + { queueIndex } + + ) : ( + + Nonce: + { nonce } + + ), position !== null && position !== undefined && ( Position: diff --git a/ui/tx/details/TxInfo.tsx b/ui/tx/details/TxInfo.tsx index 18ee2373a6..21b0871cea 100644 --- a/ui/tx/details/TxInfo.tsx +++ b/ui/tx/details/TxInfo.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { scroller, Element } from 'react-scroll'; import { ARBITRUM_L2_TX_BATCH_STATUSES } from 'types/api/arbitrumL2'; +import { SCROLL_L2_BLOCK_STATUSES } from 'types/api/scrollL2'; import type { Transaction } from 'types/api/transaction'; import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction'; import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; @@ -167,7 +168,8 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { 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' : 'Status and method' } @@ -298,6 +300,12 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } + { data.scroll?.l2_block_status && ( + <> + + + + ) } { data.zkevm_batch_number && !config.UI.views.tx.hiddenFields?.batch && ( @@ -662,6 +670,20 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } + { Boolean(data.scroll?.l1_gas_used) && ( + <> + + L1 Gas used + + + { BigNumber(data.scroll?.l1_gas_used || 0).toFormat() } + + + ) } + { !config.UI.views.tx.hiddenFields?.gas_fees && (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && ( <> @@ -769,6 +791,136 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } + { Boolean(data.scroll?.l1_fee) && ( + <> + + L1 data fee + + + + + + ) } + + { Boolean(data.scroll?.l2_fee) && ( + <> + + Execution fee + + + + + + ) } + + { Boolean(data.scroll?.l1_fee_commit_scalar) && ( + <> + + L1 commit scalar + + + + + + ) } + + { Boolean(data.scroll?.l1_fee_overhead) && ( + <> + + L1 Fee Overhead + + + + + + + + ) } + { (Boolean(data.scroll?.l1_base_fee) || Boolean(data.scroll?.l1_fee_scalar)) && ( + <> + + L1 gas fees + + + { Boolean(data.scroll?.l1_base_fee) && ( + + Base: + { BigNumber(data.scroll?.l1_base_fee).dividedBy(WEI_IN_GWEI).toFixed() } + + ) } + { Boolean(data.scroll?.l1_fee_scalar) && ( + + + Scalar: + { BigNumber(data.scroll?.l1_fee_scalar).dividedBy(WEI_IN_GWEI).toFixed() } + + ) } + + + ) } + + { (Boolean(data.scroll?.l1_blob_base_fee) || Boolean(data.scroll?.l1_fee_blob_scalar)) && ( + <> + + L1 blob fees + + + { Boolean(data.scroll?.l1_blob_base_fee) && ( + + Base: + { BigNumber(data.scroll?.l1_blob_base_fee).dividedBy(WEI_IN_GWEI).toFixed() } + + ) } + { Boolean(data.scroll?.l1_fee_blob_scalar) && ( + + + Scalar: + { BigNumber(data.scroll?.l1_fee_blob_scalar).dividedBy(WEI_IN_GWEI).toFixed() } + + ) } + + + ) } + @@ -871,7 +1023,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } - + ; +} + +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 ; + } + + if (!data) { + return null; + } + + const blocksCount = data.end_block - data.start_block + 1; + + return ( + + + Txn batch number + + + + { data.number } + + + + + + Container + + + + + + + Status + + + + + + + Finalized timestamp + + + { data.confirmation_transaction.timestamp ? + : + Pending + } + + + + Transactions + + + + + { data.transaction_count.toLocaleString() } transaction{ data.transaction_count === 1 ? '' : 's' } + + + + + + Blocks + + + + + { blocksCount.toLocaleString() } block{ blocksCount === 1 ? '' : 's' } + + + + + + Committed timestamp + + + { data.commitment_transaction.timestamp ? + : + Pending + } + + + + Committed transaction hash + + + + + + + Committed block + + + + + + + Finalized transaction hash + + + { data.confirmation_transaction.hash ? ( + + ) : Pending } + + + + Finalized block + + + { data.confirmation_transaction.block_number ? ( + + ) : Pending } + + + ); +}; + +export default ScrollL2TxnBatchDetails; diff --git a/ui/txnBatches/scrollL2/ScrollL2TxnBatchesListItem.tsx b/ui/txnBatches/scrollL2/ScrollL2TxnBatchesListItem.tsx new file mode 100644 index 0000000000..b11a04ede1 --- /dev/null +++ b/ui/txnBatches/scrollL2/ScrollL2TxnBatchesListItem.tsx @@ -0,0 +1,135 @@ +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 ( + + + Batch # + + + + + Data availability + + + + + Status + + + + + Committed block + + + + + Committed txn hash + + + + + Age + + + + + Finalized block + + { item.confirmation_transaction.block_number ? ( + + ) : Pending } + + + Finalized txn hash + + { item.confirmation_transaction.hash ? ( + + ) : Pending } + + + Blocks count + + + + { (item.end_block - item.start_block + 1).toLocaleString() } + + + + + Txn count + + + + { item.transaction_count.toLocaleString() } + + + + + + ); +}; + +export default ScrollL2TxnBatchesListItem; diff --git a/ui/txnBatches/scrollL2/ScrollL2TxnBatchesTable.tsx b/ui/txnBatches/scrollL2/ScrollL2TxnBatchesTable.tsx new file mode 100644 index 0000000000..4dd7a6151c --- /dev/null +++ b/ui/txnBatches/scrollL2/ScrollL2TxnBatchesTable.tsx @@ -0,0 +1,46 @@ +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; + top: number; + isLoading?: boolean; +}; + +const ScrollL2TxnBatchesTable = ({ items, top, isLoading }: Props) => { + return ( + + + + + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
Batch #ContainerStatusCommitted blockCommitted txn hashAgeFinalized blockFinalized txn hashBlocksTxn
+ ); +}; + +export default ScrollL2TxnBatchesTable; diff --git a/ui/txnBatches/scrollL2/ScrollL2TxnBatchesTableItem.tsx b/ui/txnBatches/scrollL2/ScrollL2TxnBatchesTableItem.tsx new file mode 100644 index 0000000000..8d7d680304 --- /dev/null +++ b/ui/txnBatches/scrollL2/ScrollL2TxnBatchesTableItem.tsx @@ -0,0 +1,114 @@ +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 ( + + + + + + + + + + + + + + + + + + + + + { item.confirmation_transaction.block_number ? ( + + ) : Pending } + + + { item.confirmation_transaction.hash ? ( + + ) : Pending } + + + + + { (item.end_block - item.start_block + 1).toLocaleString() } + + + + + + + { item.transaction_count.toLocaleString() } + + + + + ); +}; + +export default TxnBatchesTableItem; diff --git a/ui/withdrawals/scrollL2/ScrollL2WithdrawalsListItem.tsx b/ui/withdrawals/scrollL2/ScrollL2WithdrawalsListItem.tsx new file mode 100644 index 0000000000..a9b6508f5b --- /dev/null +++ b/ui/withdrawals/scrollL2/ScrollL2WithdrawalsListItem.tsx @@ -0,0 +1,94 @@ +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 ( + + + L2 block + + + + + Index + + + { item.id } + + + + L2 txn hash + + + + + Age + + + + + L1 txn hash + + { item.completion_transaction_hash ? ( + + ) : ( + + Pending Claim + + ) } + + + Value + + + { `${ valueStr } ${ config.chain.currency.symbol }` } + + + + + ); +}; + +export default ScrollL2WithdrawalsListItem; diff --git a/ui/withdrawals/scrollL2/ScrollL2WithdrawalsTable.tsx b/ui/withdrawals/scrollL2/ScrollL2WithdrawalsTable.tsx new file mode 100644 index 0000000000..c52140aa50 --- /dev/null +++ b/ui/withdrawals/scrollL2/ScrollL2WithdrawalsTable.tsx @@ -0,0 +1,39 @@ +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; + top: number; + isLoading?: boolean; + }; + +const ScrollL2WithdrawalsTable = ({ items, top, isLoading }: Props) => { + return ( + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
L2 blockIndexL2 txn hashAgeL1 txn hash{ `Value ${ config.chain.currency.symbol }` }
+ ); +}; + +export default ScrollL2WithdrawalsTable; diff --git a/ui/withdrawals/scrollL2/ScrollL2WithdrawalsTableItem.tsx b/ui/withdrawals/scrollL2/ScrollL2WithdrawalsTableItem.tsx new file mode 100644 index 0000000000..c70a3c3857 --- /dev/null +++ b/ui/withdrawals/scrollL2/ScrollL2WithdrawalsTableItem.tsx @@ -0,0 +1,82 @@ +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 ( + + + + + + + { item.id } + + + + + + + + + + { item.completion_transaction_hash ? ( + + ) : ( + + Pending Claim + + ) } + + + + { valueStr } + + + + ); +}; + +export default ScrollL2WithdrawalsTableItem;