import type { GetServerSideProps } from 'next' import { useCallback, useEffect, useMemo, useState } from 'react' import { Card, Table, Address } from '@/libs/frontend-ui-primitives' import Link from 'next/link' import { transactionsApi, Transaction } from '@/services/api/transactions' import { formatWeiAsEth } from '@/utils/format' import EntityBadge from '@/components/common/EntityBadge' import PageIntro from '@/components/common/PageIntro' import { fetchPublicJson } from '@/utils/publicExplorer' import { normalizeTransaction } from '@/services/api/blockscout' interface TransactionsPageProps { initialTransactions: Transaction[] } function serializeTransactionList(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, token_transfers: Array.isArray(transaction.token_transfers) ? transaction.token_transfers.map((transfer) => ({ token_address: transfer.token_address })) : [], })), ), ) as Transaction[] } export default function TransactionsPage({ initialTransactions }: TransactionsPageProps) { const pageSize = 20 const [transactions, setTransactions] = useState(initialTransactions) const [loading, setLoading] = useState(initialTransactions.length === 0) const [page, setPage] = useState(1) const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138') const loadTransactions = useCallback(async () => { setLoading(true) try { const { ok, data } = await transactionsApi.listSafe(chainId, page, pageSize) setTransactions(ok ? data : []) } catch (error) { console.error('Failed to load transactions:', error) setTransactions([]) } finally { setLoading(false) } }, [chainId, page, pageSize]) useEffect(() => { if (page === 1 && initialTransactions.length > 0) { setTransactions(initialTransactions) setLoading(false) return } void loadTransactions() }, [initialTransactions, loadTransactions, page]) const transactionSummary = useMemo(() => { const sampleSize = transactions.length if (sampleSize === 0) { return { sampleSize: 0, successRate: 0, contractCreations: 0, tokenTransferTransactions: 0, averageFee: null as string | null, } } const successes = transactions.filter((transaction) => transaction.status === 1).length const contractCreations = transactions.filter((transaction) => Boolean(transaction.contract_address)).length const tokenTransferTransactions = transactions.filter( (transaction) => (transaction.token_transfers?.length || 0) > 0, ).length const feeValues = transactions .map((transaction) => { if (!transaction.fee) return null const numeric = Number(transaction.fee) return Number.isFinite(numeric) ? numeric : null }) .filter((value): value is number => value != null) const averageFee = feeValues.length > 0 ? formatWeiAsEth(Math.round(feeValues.reduce((sum, value) => sum + value, 0) / feeValues.length).toString(), 6) : null return { sampleSize, successRate: Math.round((successes / sampleSize) * 100), contractCreations, tokenTransferTransactions, averageFee, } }, [transactions]) const showPagination = page > 1 || transactions.length > 0 const canGoNext = transactions.length === pageSize const columns = [ { header: 'Hash', accessor: (tx: Transaction) => (
), }, { header: 'Block', accessor: (tx: Transaction) => ( {tx.block_number} ), }, { header: 'From', accessor: (tx: Transaction) => (
), }, { header: 'To', accessor: (tx: Transaction) => tx.to_address ? (
) : Contract Creation, }, { header: 'Value', accessor: (tx: Transaction) => formatWeiAsEth(tx.value), }, { header: 'Status', accessor: (tx: Transaction) => ( {tx.status === 1 ? 'Success' : 'Failed'} ), }, ] return (
{!loading && transactions.length > 0 && (
Sample Size
{transactionSummary.sampleSize.toLocaleString()}
Transactions on the current explorer page.
Success Rate
{transactionSummary.successRate}% = 90 ? 'healthy' : 'mixed'} tone={transactionSummary.successRate >= 90 ? 'success' : 'warning'} />
Based on the visible recent transaction sample.
Contract Creations
{transactionSummary.contractCreations.toLocaleString()}
New contracts created in the visible sample.
Avg Sample Fee
{transactionSummary.averageFee || 'Unavailable'}
Token-transfer txs: {transactionSummary.tokenTransferTransactions.toLocaleString()}
)} {loading ? (

Loading transactions...

) : ( tx.hash} /> )} {showPagination && (
Page {page}
)}

Use the linked hashes above to inspect detail pages, or pivot into block production, address activity, and explorer-wide search.

Blocks → Addresses → Search →
) } export const getServerSideProps: GetServerSideProps = async () => { const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || '138') const transactionsResult = await fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=20').catch(() => null) const initialTransactions = Array.isArray(transactionsResult?.items) ? transactionsResult.items.map((item) => normalizeTransaction(item as never, chainId)) : [] return { props: { initialTransactions: serializeTransactionList(initialTransactions), }, } }