- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, 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'
|
|
|
|
export default function TransactionsPage() {
|
|
const pageSize = 20
|
|
const [transactions, setTransactions] = useState<Transaction[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
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(() => {
|
|
loadTransactions()
|
|
}, [loadTransactions])
|
|
|
|
const showPagination = page > 1 || transactions.length > 0
|
|
const canGoNext = transactions.length === pageSize
|
|
|
|
const columns = [
|
|
{
|
|
header: 'Hash',
|
|
accessor: (tx: Transaction) => (
|
|
<Link href={`/transactions/${tx.hash}`} className="text-primary-600 hover:underline">
|
|
<Address address={tx.hash} truncate showCopy={false} />
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
header: 'Block',
|
|
accessor: (tx: Transaction) => (
|
|
<Link href={`/blocks/${tx.block_number}`} className="text-primary-600 hover:underline">
|
|
{tx.block_number}
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
header: 'From',
|
|
accessor: (tx: Transaction) => (
|
|
<Link href={`/addresses/${tx.from_address}`} className="text-primary-600 hover:underline">
|
|
<Address address={tx.from_address} truncate showCopy={false} />
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
header: 'To',
|
|
accessor: (tx: Transaction) => tx.to_address ? (
|
|
<Link href={`/addresses/${tx.to_address}`} className="text-primary-600 hover:underline">
|
|
<Address address={tx.to_address} truncate showCopy={false} />
|
|
</Link>
|
|
) : <span className="text-gray-400">Contract Creation</span>,
|
|
},
|
|
{
|
|
header: 'Value',
|
|
accessor: (tx: Transaction) => formatWeiAsEth(tx.value),
|
|
},
|
|
{
|
|
header: 'Status',
|
|
accessor: (tx: Transaction) => (
|
|
<span className={tx.status === 1 ? 'text-green-600' : 'text-red-600'}>
|
|
{tx.status === 1 ? 'Success' : 'Failed'}
|
|
</span>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:py-8">
|
|
<h1 className="mb-6 text-3xl font-bold">Transactions</h1>
|
|
|
|
{loading ? (
|
|
<Card>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Loading transactions...</p>
|
|
</Card>
|
|
) : (
|
|
<Table
|
|
columns={columns}
|
|
data={transactions}
|
|
emptyMessage="Recent transactions are unavailable right now."
|
|
keyExtractor={(tx) => tx.hash}
|
|
/>
|
|
)}
|
|
|
|
{showPagination && (
|
|
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
|
|
<button
|
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
disabled={loading || page === 1}
|
|
className="rounded bg-gray-200 px-4 py-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Previous
|
|
</button>
|
|
<span className="px-3 py-2 text-sm sm:px-4">Page {page}</span>
|
|
<button
|
|
onClick={() => setPage((p) => p + 1)}
|
|
disabled={loading || !canGoNext}
|
|
className="rounded bg-gray-200 px-4 py-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|