Commit 7d70f119 authored by isstuev's avatar isstuev

arbitrum latest batches and deposits

parent 02556490
...@@ -43,10 +43,12 @@ import type { AddressesResponse } from 'types/api/addresses'; ...@@ -43,10 +43,12 @@ import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type { import type {
ArbitrumL2MessagesResponse, ArbitrumL2MessagesResponse,
ArbitrumL2MessagesItem,
ArbitrumL2TxnBatch, ArbitrumL2TxnBatch,
ArbitrumL2TxnBatchesResponse, ArbitrumL2TxnBatchesResponse,
ArbitrumL2BatchTxs, ArbitrumL2BatchTxs,
ArbitrumL2BatchBlocks, ArbitrumL2BatchBlocks,
ArbitrumL2TxnBatchesItem,
} from 'types/api/arbitrumL2'; } from 'types/api/arbitrumL2';
import type { TxBlobs, Blob } from 'types/api/blobs'; import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block';
...@@ -580,15 +582,21 @@ export const RESOURCES = { ...@@ -580,15 +582,21 @@ export const RESOURCES = {
homepage_blocks: { homepage_blocks: {
path: '/api/v2/main-page/blocks', path: '/api/v2/main-page/blocks',
}, },
homepage_deposits: { homepage_optimistic_deposits: {
path: '/api/v2/main-page/optimism-deposits', path: '/api/v2/main-page/optimism-deposits',
}, },
homepage_arbitrum_deposits: {
path: '/api/v2/main-page/arbitrum/messages/to-rollup',
},
homepage_txs: { homepage_txs: {
path: '/api/v2/main-page/transactions', path: '/api/v2/main-page/transactions',
}, },
homepage_zkevm_l2_batches: { homepage_zkevm_l2_batches: {
path: '/api/v2/main-page/zkevm/batches/confirmed', path: '/api/v2/main-page/zkevm/batches/confirmed',
}, },
homepage_arbitrum_l2_batches: {
path: '/api/v2/main-page/arbitrum/batches/committed',
},
homepage_txs_watchlist: { homepage_txs_watchlist: {
path: '/api/v2/main-page/transactions/watchlist', path: '/api/v2/main-page/transactions/watchlist',
}, },
...@@ -601,6 +609,9 @@ export const RESOURCES = { ...@@ -601,6 +609,9 @@ export const RESOURCES = {
homepage_zksync_latest_batch: { homepage_zksync_latest_batch: {
path: '/api/v2/main-page/zksync/batches/latest-number', path: '/api/v2/main-page/zksync/batches/latest-number',
}, },
homepage_arbitrum_latest_batch: {
path: '/api/v2/main-page/arbitrum/batches/latest-number',
},
// SEARCH // SEARCH
quick_search: { quick_search: {
...@@ -966,11 +977,14 @@ Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse ...@@ -966,11 +977,14 @@ Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse
Q extends 'homepage_blocks' ? Array<Block> : Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> : Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<OptimisticL2DepositsItem> : Q extends 'homepage_optimistic_deposits' ? Array<OptimisticL2DepositsItem> :
Q extends 'homepage_arbitrum_deposits' ? { items: Array<ArbitrumL2MessagesItem> } :
Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } : Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
Q extends 'homepage_arbitrum_l2_batches' ? { items: Array<ArbitrumL2TxnBatchesItem>} :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'homepage_zkevm_latest_batch' ? number : Q extends 'homepage_zkevm_latest_batch' ? number :
Q extends 'homepage_zksync_latest_batch' ? number : Q extends 'homepage_zksync_latest_batch' ? number :
Q extends 'homepage_arbitrum_latest_batch' ? number :
Q extends 'stats_counters' ? stats.Counters : Q extends 'stats_counters' ? stats.Counters :
Q extends 'stats_lines' ? stats.LineCharts : Q extends 'stats_lines' ? stats.LineCharts :
Q extends 'stats_line' ? stats.LineChart : Q extends 'stats_line' ? stats.LineChart :
...@@ -1029,8 +1043,6 @@ Q extends 'verified_contracts' ? VerifiedContractsResponse : ...@@ -1029,8 +1043,6 @@ Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters : Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse : Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse :
Q extends 'contract_verification_config' ? SmartContractVerificationConfigRaw : Q extends 'contract_verification_config' ? SmartContractVerificationConfigRaw :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse : Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse : Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse : Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
...@@ -1097,6 +1109,8 @@ Q extends 'address_mud_tables' ? AddressMudTables : ...@@ -1097,6 +1109,8 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number : Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords : Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord : Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
import type { Channel } from 'phoenix'; import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address';
import type { NewArbitrumBatchSocketResponse } from 'types/api/arbitrumL2';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract'; import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
...@@ -16,7 +17,8 @@ SocketMessage.TxStatusUpdate | ...@@ -16,7 +17,8 @@ SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace | SocketMessage.TxRawTrace |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
SocketMessage.NewDeposits | SocketMessage.NewOptimisticDeposits |
SocketMessage.NewArbitrumDeposits |
SocketMessage.AddressBalance | SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance | SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance | SocketMessage.AddressTokenBalance |
...@@ -36,6 +38,7 @@ SocketMessage.TokenTotalSupply | ...@@ -36,6 +38,7 @@ SocketMessage.TokenTotalSupply |
SocketMessage.TokenInstanceMetadataFetched | SocketMessage.TokenInstanceMetadataFetched |
SocketMessage.ContractVerification | SocketMessage.ContractVerification |
SocketMessage.NewZkEvmL2Batch | SocketMessage.NewZkEvmL2Batch |
SocketMessage.NewArbitrumL2Batch |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -53,7 +56,8 @@ export namespace SocketMessage { ...@@ -53,7 +56,8 @@ export namespace SocketMessage {
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>; export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type NewDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>; export type NewOptimisticDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>;
export type NewArbitrumDeposits = SocketMessageParamsGeneric<'new_messages_to_rollup_amount', { new_messages_to_rollup_amount: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>; export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance = export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
...@@ -74,5 +78,6 @@ export namespace SocketMessage { ...@@ -74,5 +78,6 @@ export namespace SocketMessage {
export type TokenInstanceMetadataFetched = SocketMessageParamsGeneric<'fetched_token_instance_metadata', TokenInstanceMetadataSocketMessage>; export type TokenInstanceMetadataFetched = SocketMessageParamsGeneric<'fetched_token_instance_metadata', TokenInstanceMetadataSocketMessage>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>; export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>; export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>;
export type NewArbitrumL2Batch = SocketMessageParamsGeneric<'new_arbitrum_batch', NewArbitrumBatchSocketResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -84,3 +84,5 @@ export const ARBITRUM_L2_TX_BATCH_STATUSES = [ ...@@ -84,3 +84,5 @@ export const ARBITRUM_L2_TX_BATCH_STATUSES = [
]; ];
export type ArbitrumBatchStatus = typeof ARBITRUM_L2_TX_BATCH_STATUSES[number]; export type ArbitrumBatchStatus = typeof ARBITRUM_L2_TX_BATCH_STATUSES[number];
export type NewArbitrumBatchSocketResponse = { batch: ArbitrumL2TxnBatchesItem }
...@@ -46,13 +46,21 @@ const Stats = () => { ...@@ -46,13 +46,21 @@ const Stats = () => {
}, },
}); });
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError) { const arbitrumLatestBatchQuery = useApiQuery('homepage_arbitrum_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'arbitrum',
},
});
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError || arbitrumLatestBatchQuery.isError) {
return null; return null;
} }
const isLoading = isPlaceholderData || const isLoading = isPlaceholderData ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) || (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData); (rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && arbitrumLatestBatchQuery.isPlaceholderData);
const content = (() => { const content = (() => {
if (!data) { if (!data) {
...@@ -72,22 +80,21 @@ const Stats = () => { ...@@ -72,22 +80,21 @@ const Stats = () => {
</GasInfoTooltip> </GasInfoTooltip>
) : null; ) : null;
const hasBatches = rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum');
const latestBatch =
(hasBatches && rollupFeature.type === 'zkEvm' ? zkEvmLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'zkSync' ? zkSyncLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'arbitrum' ? arbitrumLatestBatchQuery.data : null) || 0;
const items: Array<StatsWidgetProps> = [ const items: Array<StatsWidgetProps> = [
rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && { hasBatches && {
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: (zkEvmLatestBatchQuery.data || 0).toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && {
icon: 'txn_batches_slim' as const, icon: 'txn_batches_slim' as const,
label: 'Latest batch', label: 'Latest batch',
value: (zkSyncLatestBatchQuery.data || 0).toLocaleString(), value: latestBatch.toLocaleString(),
href: { pathname: '/batches' as const }, href: { pathname: '/batches' as const },
isLoading, isLoading,
}, },
!(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && { !hasBatches && {
icon: 'block_slim' as const, icon: 'block_slim' as const,
label: 'Total blocks', label: 'Total blocks',
value: Number(data.total_blocks).toLocaleString(), value: Number(data.total_blocks).toLocaleString(),
......
...@@ -3,10 +3,13 @@ import React from 'react'; ...@@ -3,10 +3,13 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useHasAccount from 'lib/hooks/useHasAccount'; import useHasAccount from 'lib/hooks/useHasAccount';
import LatestDeposits from 'ui/home/LatestDeposits'; import LatestOptimisticDeposits from 'ui/home/latestDeposits/LatestOptimisticDeposits';
import LatestTxs from 'ui/home/LatestTxs'; import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs'; import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import LatestArbitrumDeposits from './latestDeposits/LatestArbitrumDeposits';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
...@@ -15,10 +18,13 @@ const TAB_LIST_PROPS = { ...@@ -15,10 +18,13 @@ const TAB_LIST_PROPS = {
const TransactionsHome = () => { const TransactionsHome = () => {
const hasAccount = useHasAccount(); const hasAccount = useHasAccount();
if ((rollupFeature.isEnabled && rollupFeature.type === 'optimistic') || hasAccount) { if ((rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum')) || hasAccount) {
const tabs = [ const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> }, { id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> }, rollupFeature.isEnabled && rollupFeature.type === 'optimistic' &&
{ id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestOptimisticDeposits/> },
rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' &&
{ id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestArbitrumDeposits/> },
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> }, hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean); ].filter(Boolean);
return ( return (
......
import React from 'react';
import { finalized, unfinalized } from 'mocks/arbitrum/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import LatestArbitrumL2Batches from './LatestArbitrumL2Batches';
test('default view +@mobile +@dark-mode', async({ render, mockEnvs, mockApiResponse }) => {
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockApiResponse('homepage_arbitrum_l2_batches', { items: [ finalized, unfinalized ] });
const component = await render(<LatestArbitrumL2Batches/>);
await expect(component).toHaveScreenshot();
});
import { Box, Heading, Flex, Text, VStack } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ARBITRUM_L2_TXN_BATCHES_ITEM } from 'stubs/arbitrumL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import LatestBatchItem from './LatestBatchItem';
const LatestArbitrumL2Batches = () => {
const isMobile = useIsMobile();
const batchesMaxCount = isMobile ? 2 : 5;
const queryClient = useQueryClient();
const { data, isPlaceholderData, isError } = useApiQuery('homepage_arbitrum_l2_batches', {
queryOptions: {
placeholderData: { items: Array(batchesMaxCount).fill(ARBITRUM_L2_TXN_BATCHES_ITEM) },
},
});
const handleNewBatchMessage: SocketMessage.NewArbitrumL2Batch['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_arbitrum_l2_batches'), (prevData: { items: Array<ArbitrumL2TxnBatchesItem> } | undefined) => {
const newItems = prevData?.items ? [ ...prevData.items ] : [];
if (newItems.some((batch => batch.number === payload.batch.number))) {
return { items: newItems };
}
return { items: [ payload.batch, ...newItems ].sort((b1, b2) => b2.number - b1.number).slice(0, batchesMaxCount) };
});
}, [ queryClient, batchesMaxCount ]);
const channel = useSocketChannel({
topic: 'arbitrum:new_batch',
isDisabled: isPlaceholderData || isError,
});
useSocketMessage({
channel,
event: 'new_arbitrum_batch',
handler: handleNewBatchMessage,
});
let content;
if (isError) {
content = <Text>No data. Please reload page.</Text>;
}
if (data) {
const dataToShow = data.items.slice(0, batchesMaxCount);
content = (
<>
<VStack spacing={ 2 } mb={ 3 } overflow="hidden" alignItems="stretch">
<AnimatePresence initial={ false } >
{ dataToShow.map(((batch, index) => (
<LatestBatchItem
key={ batch.number + (isPlaceholderData ? String(index) : '') }
number={ batch.number }
timestamp={ batch.commitment_transaction.timestamp }
txCount={ batch.transactions_count }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ route({ pathname: '/batches' }) }>View all batches</LinkInternal>
</Flex>
</>
);
}
return (
<Box width={{ base: '100%', lg: '280px' }} flexShrink={ 0 }>
<Heading as="h4" size="sm" mb={ 3 }>Latest batches</Heading>
{ content }
</Box>
);
};
export default LatestArbitrumL2Batches;
...@@ -6,21 +6,21 @@ import { ...@@ -6,21 +6,21 @@ import {
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import React from 'react'; import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type Props = { type Props = {
batch: ZkEvmL2TxnBatchesItem; number: number;
isLoading?: boolean; timestamp: string | null;
txCount: number;
status?: React.ReactNode;
isLoading: boolean;
} }
const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => { const LatestBatchItem = ({ number, timestamp, txCount, status, isLoading }: Props) => {
return ( return (
<Box <Box
as={ motion.div } as={ motion.div }
...@@ -37,7 +37,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => { ...@@ -37,7 +37,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
<Flex alignItems="center" overflow="hidden" w="100%" mb={ 3 }> <Flex alignItems="center" overflow="hidden" w="100%" mb={ 3 }>
<BatchEntityL2 <BatchEntityL2
isLoading={ isLoading } isLoading={ isLoading }
number={ batch.number } number={ number }
tailLength={ 2 } tailLength={ 2 }
fontSize="xl" fontSize="xl"
lineHeight={ 7 } lineHeight={ 7 }
...@@ -45,7 +45,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => { ...@@ -45,7 +45,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
mr="auto" mr="auto"
/> />
<TimeAgoWithTooltip <TimeAgoWithTooltip
timestamp={ batch.timestamp } timestamp={ timestamp }
enableIncrement={ !isLoading } enableIncrement={ !isLoading }
isLoading={ isLoading } isLoading={ isLoading }
color="text_secondary" color="text_secondary"
...@@ -60,18 +60,18 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => { ...@@ -60,18 +60,18 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
<Flex alignItems="center"> <Flex alignItems="center">
<Skeleton isLoaded={ !isLoading } mr={ 2 }>Txn</Skeleton> <Skeleton isLoaded={ !isLoading } mr={ 2 }>Txn</Skeleton>
<LinkInternal <LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: batch.number.toString(), tab: 'txs' } }) } href={ route({ pathname: '/batches/[number]', query: { number: number.toString(), tab: 'txs' } }) }
isLoading={ isLoading } isLoading={ isLoading }
> >
<Skeleton isLoaded={ !isLoading }> <Skeleton isLoaded={ !isLoading }>
{ batch.tx_count } { txCount }
</Skeleton> </Skeleton>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
<ZkEvmL2TxnBatchStatus status={ batch.status } isLoading={ isLoading }/> { status }
</Flex> </Flex>
</Box> </Box>
); );
}; };
export default LatestZkevmL2BatchItem; export default LatestBatchItem;
...@@ -14,8 +14,9 @@ import useSocketChannel from 'lib/socket/useSocketChannel'; ...@@ -14,8 +14,9 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { ZKEVM_L2_TXN_BATCHES_ITEM } from 'stubs/zkEvmL2'; import { ZKEVM_L2_TXN_BATCHES_ITEM } from 'stubs/zkEvmL2';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
import LatestZkevmL2BatchItem from './LatestZkevmL2BatchItem'; import LatestBatchItem from './LatestBatchItem';
const LatestZkEvmL2Batches = () => { const LatestZkEvmL2Batches = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -63,13 +64,19 @@ const LatestZkEvmL2Batches = () => { ...@@ -63,13 +64,19 @@ const LatestZkEvmL2Batches = () => {
<> <>
<VStack spacing={ 2 } mb={ 3 } overflow="hidden" alignItems="stretch"> <VStack spacing={ 2 } mb={ 3 } overflow="hidden" alignItems="stretch">
<AnimatePresence initial={ false } > <AnimatePresence initial={ false } >
{ dataToShow.map(((batch, index) => ( { dataToShow.map(((batch, index) => {
<LatestZkevmL2BatchItem const status = <ZkEvmL2TxnBatchStatus status={ batch.status } isLoading={ isPlaceholderData }/>;
return (
<LatestBatchItem
key={ batch.number + (isPlaceholderData ? String(index) : '') } key={ batch.number + (isPlaceholderData ? String(index) : '') }
batch={ batch } number={ batch.number }
txCount={ batch.tx_count }
timestamp={ batch.timestamp }
status={ status }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
))) } );
})) }
</AnimatePresence> </AnimatePresence>
</VStack> </VStack>
<Flex justifyContent="center"> <Flex justifyContent="center">
......
import { Text } from '@chakra-ui/react';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ARBITRUM_MESSAGES_ITEM } from 'stubs/arbitrumL2';
import LatestDeposits from './LatestDeposits';
const LatestArbitrumDeposits = () => {
const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6;
const { data, isPlaceholderData, isError } = useApiQuery('homepage_arbitrum_deposits', {
queryOptions: {
placeholderData: { items: Array(itemsCount).fill(ARBITRUM_MESSAGES_ITEM) },
},
});
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload page.');
}, []);
const handleNewDepositMessage: SocketMessage.NewArbitrumDeposits['handler'] = React.useCallback((payload) => {
setNum(payload.new_messages_to_rollup_amount);
}, [ setNum ]);
const channel = useSocketChannel({
topic: 'arbitrum:new_messages_to_rollup_amount',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'new_messages_to_rollup_amount',
handler: handleNewDepositMessage,
});
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
if (data) {
return (
<LatestDeposits
items={ data.items.slice(0, itemsCount).map((item) => (
{
l1BlockNumber: item.origination_transaction_block_number,
l1TxHash: item.origination_transaction_hash,
l2TxHash: item.completion_transaction_hash,
timestamp: item.origination_timestamp,
}
)) }
isLoading={ isPlaceholderData }
socketItemsNum={ num }
socketAlert={ socketAlert }
/>
);
}
return null;
};
export default LatestArbitrumDeposits;
import {
Box,
Flex,
Grid,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile';
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 LinkInternal from 'ui/shared/links/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type DepositsItem = {
l1BlockNumber: number;
l1TxHash: string;
l2TxHash: string | null;
timestamp: string | null;
}
type Props = {
isLoading?: boolean;
items: Array<DepositsItem>;
socketItemsNum: number;
socketAlert?: string;
}
type ItemProps = {
item: DepositsItem;
isLoading?: boolean;
}
const LatestDepositsItem = ({ item, isLoading }: ItemProps) => {
const isMobile = useIsMobile();
const l1BlockLink = (
<BlockEntityL1
number={ item.l1BlockNumber }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 700 }
/>
);
const l1TxLink = (
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
);
const l2TxLink = item.l2TxHash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.l2TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
) : null;
const content = (() => {
if (isMobile) {
return (
<>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink }
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
/>
</Flex>
<Grid gridTemplateColumns="56px auto">
<Skeleton isLoaded={ !isLoading } my="5px" w="fit-content">
L1 txn
</Skeleton>
{ l1TxLink }
<Skeleton isLoaded={ !isLoading } my="3px" w="fit-content">
L2 txn
</Skeleton>
{ l2TxLink }
</Grid>
</>
);
}
return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink }
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="5px">
L1 txn
</Skeleton>
{ l1TxLink }
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
w="fit-content"
h="fit-content"
my="2px"
/>
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="2px">
L2 txn
</Skeleton>
{ l2TxLink }
</Grid>
);
})();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm"
lineHeight={ 5 }
>
{ content }
</Box>
);
};
const LatestDeposits = ({ isLoading, items, socketAlert, socketItemsNum }: Props) => {
const depositsUrl = route({ pathname: '/deposits' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ socketItemsNum } alert={ socketAlert } type="deposit" isLoading={ isLoading }/>
<Box mb={{ base: 3, lg: 4 }}>
{ items.map(((item, index) => (
<LatestDepositsItem
key={ item.l1TxHash + item.l2TxHash + (isLoading ? index : '') }
item={ item }
isLoading={ isLoading }
/>
))) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
</Flex>
</>
);
};
export default LatestDeposits;
...@@ -6,32 +6,26 @@ import { ...@@ -6,32 +6,26 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1'; import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const feature = config.features.rollup;
type Props = { type Props = {
item: OptimisticL2DepositsItem; l1BlockNumber: number;
l1TxHash: string;
l2TxHash: string | null;
timestamp: string | null;
isLoading?: boolean; isLoading?: boolean;
} }
const LatestDepositsItem = ({ item, isLoading }: Props) => { const LatestDepositsItem = ({ l1BlockNumber, l1TxHash, l2TxHash, timestamp, isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (!feature.isEnabled || feature.type !== 'optimistic') {
return null;
}
const l1BlockLink = ( const l1BlockLink = (
<BlockEntityL1 <BlockEntityL1
number={ item.l1_block_number } number={ l1BlockNumber }
isLoading={ isLoading } isLoading={ isLoading }
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
...@@ -42,22 +36,22 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => { ...@@ -42,22 +36,22 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
const l1TxLink = ( const l1TxLink = (
<TxEntityL1 <TxEntityL1
isLoading={ isLoading } isLoading={ isLoading }
hash={ item.l1_tx_hash } hash={ l1TxHash }
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' } truncation={ isMobile ? 'constant_long' : 'dynamic' }
/> />
); );
const l2TxLink = ( const l2TxLink = l2TxHash ? (
<TxEntity <TxEntity
isLoading={ isLoading } isLoading={ isLoading }
hash={ item.l2_tx_hash } hash={ l2TxHash }
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' } truncation={ isMobile ? 'constant_long' : 'dynamic' }
/> />
); ) : null;
const content = (() => { const content = (() => {
if (isMobile) { if (isMobile) {
...@@ -66,7 +60,7 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => { ...@@ -66,7 +60,7 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }> <Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink } { l1BlockLink }
<TimeAgoWithTooltip <TimeAgoWithTooltip
timestamp={ item.l1_block_timestamp } timestamp={ timestamp }
isLoading={ isLoading } isLoading={ isLoading }
color="text_secondary" color="text_secondary"
/> />
...@@ -93,7 +87,7 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => { ...@@ -93,7 +87,7 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
</Skeleton> </Skeleton>
{ l1TxLink } { l1TxLink }
<TimeAgoWithTooltip <TimeAgoWithTooltip
timestamp={ item.l1_block_timestamp } timestamp={ timestamp }
isLoading={ isLoading } isLoading={ isLoading }
color="text_secondary" color="text_secondary"
w="fit-content" w="fit-content"
......
...@@ -4,11 +4,11 @@ import * as depositMock from 'mocks/l2deposits/deposits'; ...@@ -4,11 +4,11 @@ import * as depositMock from 'mocks/l2deposits/deposits';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import LatestDeposits from './LatestDeposits'; import LatestOptimisticDeposits from './LatestOptimisticDeposits';
test('default view +@mobile +@dark-mode', async({ render, mockApiResponse, mockEnvs }) => { test('default view +@mobile +@dark-mode', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup); await mockEnvs(ENVS_MAP.optimisticRollup);
mockApiResponse('homepage_deposits', depositMock.data.items); mockApiResponse('homepage_optimistic_deposits', depositMock.data.items);
const component = await render(<LatestDeposits/>); const component = await render(<LatestOptimisticDeposits/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Box, Flex, Text } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { L2_DEPOSIT_ITEM } from 'stubs/L2'; import { L2_DEPOSIT_ITEM } from 'stubs/L2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem'; import LatestDeposits from './LatestDeposits';
const LatestDeposits = () => { const LatestOptimisticDeposits = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6; const itemsCount = isMobile ? 2 : 6;
const { data, isPlaceholderData, isError } = useApiQuery('homepage_deposits', { const { data, isPlaceholderData, isError } = useApiQuery('homepage_optimistic_deposits', {
queryOptions: { queryOptions: {
placeholderData: Array(itemsCount).fill(L2_DEPOSIT_ITEM), placeholderData: Array(itemsCount).fill(L2_DEPOSIT_ITEM),
}, },
...@@ -36,7 +32,7 @@ const LatestDeposits = () => { ...@@ -36,7 +32,7 @@ const LatestDeposits = () => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload page.'); setSocketAlert('An error has occurred while fetching new transactions. Please reload page.');
}, []); }, []);
const handleNewDepositMessage: SocketMessage.NewDeposits['handler'] = React.useCallback((payload) => { const handleNewDepositMessage: SocketMessage.NewOptimisticDeposits['handler'] = React.useCallback((payload) => {
setNum(payload.deposits); setNum(payload.deposits);
}, [ setNum ]); }, [ setNum ]);
...@@ -58,27 +54,19 @@ const LatestDeposits = () => { ...@@ -58,27 +54,19 @@ const LatestDeposits = () => {
} }
if (data) { if (data) {
const depositsUrl = route({ pathname: '/deposits' });
return ( return (
<> <LatestDeposits
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit" isLoading={ isPlaceholderData }/> items={ data.slice(0, itemsCount).map((item) => (
<Box mb={{ base: 3, lg: 4 }}> { l1BlockNumber: item.l1_block_number, l1TxHash: item.l1_tx_hash, l2TxHash: item.l2_tx_hash, timestamp: item.l1_block_timestamp }
{ data.slice(0, itemsCount).map(((item, index) => ( )) }
<LatestDepositsItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
socketItemsNum={ num }
socketAlert={ socketAlert }
/> />
))) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
</Flex>
</>
); );
} }
return null; return null;
}; };
export default LatestDeposits; export default LatestOptimisticDeposits;
...@@ -4,8 +4,9 @@ import React from 'react'; ...@@ -4,8 +4,9 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import ChainIndicators from 'ui/home/indicators/ChainIndicators'; import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestArbitrumL2Batches from 'ui/home/latestBatches/LatestArbitrumL2Batches';
import LatestZkEvmL2Batches from 'ui/home/latestBatches/LatestZkEvmL2Batches';
import LatestBlocks from 'ui/home/LatestBlocks'; import LatestBlocks from 'ui/home/LatestBlocks';
import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches';
import Stats from 'ui/home/Stats'; import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions'; import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
...@@ -61,7 +62,9 @@ const Home = () => { ...@@ -61,7 +62,9 @@ const Home = () => {
</Flex> </Flex>
{ isMobile && <AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/> } { isMobile && <AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/> }
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }> <Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? <LatestZkEvmL2Batches/> : <LatestBlocks/> } { rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && <LatestZkEvmL2Batches/> }
{ rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && <LatestArbitrumL2Batches/> }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'arbitrum' || rollupFeature.type === 'zkEvm')) && <LatestBlocks/> }
<Box flexGrow={ 1 }> <Box flexGrow={ 1 }>
<Transactions/> <Transactions/>
</Box> </Box>
......
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