import type { GetServerSideProps } from 'next' import { useCallback, useEffect, useMemo, useState } from 'react' import { blocksApi, Block } from '@/services/api/blocks' import { Card, Address } from '@/libs/frontend-ui-primitives' import Link from 'next/link' import PageIntro from '@/components/common/PageIntro' import { formatTimestamp } from '@/utils/format' import { fetchPublicJson } from '@/utils/publicExplorer' import { normalizeBlock, normalizeTransaction } from '@/services/api/blockscout' import type { Transaction } from '@/services/api/transactions' import { transactionsApi } from '@/services/api/transactions' import { summarizeChainActivity } from '@/utils/activityContext' import ActivityContextPanel from '@/components/common/ActivityContextPanel' import FreshnessTrustNote from '@/components/common/FreshnessTrustNote' import { normalizeExplorerStats, type ExplorerStats } from '@/services/api/stats' import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl' import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness' interface BlocksPageProps { initialBlocks: Block[] initialRecentTransactions: Transaction[] initialStats: ExplorerStats | null initialBridgeStatus: MissionControlBridgeStatusResponse | null } function serializeTransactions(transactions: Transaction[]): Transaction[] { return JSON.parse( JSON.stringify( transactions.map((transaction) => ({ hash: transaction.hash, block_number: transaction.block_number, from_address: transaction.from_address, to_address: transaction.to_address ?? null, value: transaction.value, status: transaction.status ?? null, contract_address: transaction.contract_address ?? null, fee: transaction.fee ?? null, created_at: transaction.created_at, })), ), ) as Transaction[] } export default function BlocksPage({ initialBlocks, initialRecentTransactions, initialStats, initialBridgeStatus, }: BlocksPageProps) { const pageSize = 20 const [blocks, setBlocks] = useState(initialBlocks) const [recentTransactions, setRecentTransactions] = useState(initialRecentTransactions) const [loading, setLoading] = useState(initialBlocks.length === 0) const [page, setPage] = useState(1) const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138') const loadBlocks = useCallback(async () => { setLoading(true) try { const response = await blocksApi.list({ chain_id: chainId, page, page_size: pageSize, sort: 'number', order: 'desc', }) setBlocks(response.data) } catch (error) { console.error('Failed to load blocks:', error) setBlocks([]) } finally { setLoading(false) } }, [chainId, page, pageSize]) useEffect(() => { if (page === 1 && initialBlocks.length > 0) { setBlocks(initialBlocks) setLoading(false) return } void loadBlocks() }, [initialBlocks, loadBlocks, page]) useEffect(() => { if (initialRecentTransactions.length > 0) { setRecentTransactions(initialRecentTransactions) return } let active = true transactionsApi.listSafe(chainId, 1, 5) .then(({ ok, data }) => { if (active && ok && data.length > 0) { setRecentTransactions(data) } }) .catch(() => { if (active) { setRecentTransactions([]) } }) return () => { active = false } }, [chainId, initialRecentTransactions]) const showPagination = page > 1 || blocks.length > 0 const canGoNext = blocks.length === pageSize const activityContext = useMemo( () => summarizeChainActivity({ blocks, transactions: recentTransactions, latestBlockNumber: blocks[0]?.number ?? null, latestBlockTimestamp: blocks[0]?.timestamp ?? null, freshness: resolveEffectiveFreshness(initialStats, initialBridgeStatus), diagnostics: initialStats?.diagnostics ?? initialBridgeStatus?.data?.diagnostics ?? null, }), [blocks, initialBridgeStatus, initialStats, recentTransactions], ) return (
{loading ? (

Loading blocks...

) : (
{shouldExplainEmptyHeadBlocks(blocks, activityContext) ? (

Recent head blocks are currently empty; use the latest transaction block for recent visible activity.

) : null} {blocks.length === 0 ? (

Recent blocks are unavailable right now.

Open recent transactions → Search by block number →
) : ( blocks.map((block) => (
Block #{block.number}
Miner:{' '}
{formatTimestamp(block.timestamp)}
{block.transaction_count} transactions
)) )}
)} {showPagination && (
Page {page}
)}

Need a different entry point? Open transaction flow, search directly by block number, or jump into recently active addresses.

Transactions → Addresses → Search →
) } export const getServerSideProps: GetServerSideProps = async () => { const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || '138') const [blocksResult, transactionsResult, statsResult, bridgeResult] = await Promise.all([ fetchPublicJson<{ items?: unknown[] }>('/api/v2/blocks?page=1&page_size=20').catch(() => null), fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=5').catch(() => null), fetchPublicJson>('/api/v2/stats').catch(() => null), fetchPublicJson('/explorer-api/v1/track1/bridge/status').catch(() => null), ]) return { props: { initialBlocks: Array.isArray(blocksResult?.items) ? blocksResult.items.map((item) => normalizeBlock(item as never, chainId)) : [], initialRecentTransactions: Array.isArray(transactionsResult?.items) ? serializeTransactions(transactionsResult.items.map((item) => normalizeTransaction(item as never, chainId))) : [], initialStats: statsResult ? normalizeExplorerStats(statsResult as never) : null, initialBridgeStatus: bridgeResult, }, } }