feat: explorer API, wallet, CCIP scripts, and config refresh
- 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
This commit is contained in:
177
frontend/src/components/explorer/AnalyticsOperationsPage.tsx
Normal file
177
frontend/src/components/explorer/AnalyticsOperationsPage.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { explorerFeaturePages } from '@/data/explorerOperations'
|
||||
import { blocksApi, type Block } from '@/services/api/blocks'
|
||||
import {
|
||||
missionControlApi,
|
||||
type MissionControlBridgeStatusResponse,
|
||||
type MissionControlChainStatus,
|
||||
} from '@/services/api/missionControl'
|
||||
import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
import { transactionsApi, type Transaction } from '@/services/api/transactions'
|
||||
import OperationsPageShell, {
|
||||
MetricCard,
|
||||
StatusBadge,
|
||||
formatNumber,
|
||||
relativeAge,
|
||||
truncateMiddle,
|
||||
} from './OperationsPageShell'
|
||||
|
||||
function getChainStatus(bridgeStatus: MissionControlBridgeStatusResponse | null): MissionControlChainStatus | null {
|
||||
const chains = bridgeStatus?.data?.chains
|
||||
if (!chains) return null
|
||||
const [firstChain] = Object.values(chains)
|
||||
return firstChain || null
|
||||
}
|
||||
|
||||
export default function AnalyticsOperationsPage() {
|
||||
const [stats, setStats] = useState<ExplorerStats | null>(null)
|
||||
const [blocks, setBlocks] = useState<Block[]>([])
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([])
|
||||
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
|
||||
const [loadingError, setLoadingError] = useState<string | null>(null)
|
||||
const page = explorerFeaturePages.analytics
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const load = async () => {
|
||||
const [statsResult, blocksResult, transactionsResult, bridgeResult] = await Promise.allSettled([
|
||||
statsApi.get(),
|
||||
blocksApi.list({ chain_id: 138, page: 1, page_size: 5 }),
|
||||
transactionsApi.list(138, 1, 5),
|
||||
missionControlApi.getBridgeStatus(),
|
||||
])
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
if (statsResult.status === 'fulfilled') setStats(statsResult.value)
|
||||
if (blocksResult.status === 'fulfilled') setBlocks(blocksResult.value.data)
|
||||
if (transactionsResult.status === 'fulfilled') setTransactions(transactionsResult.value.data)
|
||||
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
|
||||
|
||||
const failedCount = [statsResult, blocksResult, transactionsResult, bridgeResult].filter(
|
||||
(result) => result.status === 'rejected'
|
||||
).length
|
||||
|
||||
if (failedCount === 4) {
|
||||
setLoadingError('Analytics data is temporarily unavailable from the public explorer APIs.')
|
||||
}
|
||||
}
|
||||
|
||||
load().catch((error) => {
|
||||
if (!cancelled) {
|
||||
setLoadingError(error instanceof Error ? error.message : 'Analytics data is temporarily unavailable from the public explorer APIs.')
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const chainStatus = useMemo(() => getChainStatus(bridgeStatus), [bridgeStatus])
|
||||
|
||||
return (
|
||||
<OperationsPageShell page={page}>
|
||||
{loadingError ? (
|
||||
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard
|
||||
title="Total Blocks"
|
||||
value={formatNumber(stats?.total_blocks)}
|
||||
description="Current block count from the public Blockscout stats endpoint."
|
||||
className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Transactions"
|
||||
value={formatNumber(stats?.total_transactions)}
|
||||
description="Total transactions currently indexed by the public explorer."
|
||||
className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Addresses"
|
||||
value={formatNumber(stats?.total_addresses)}
|
||||
description="Known addresses from the public stats surface."
|
||||
/>
|
||||
<MetricCard
|
||||
title="Chain Head"
|
||||
value={chainStatus?.head_age_sec != null ? `${Math.round(chainStatus.head_age_sec)}s` : 'Unknown'}
|
||||
description={
|
||||
chainStatus?.latency_ms != null
|
||||
? `RPC latency ${Math.round(chainStatus.latency_ms)}ms on Chain 138.`
|
||||
: 'Latest public RPC head age from mission control.'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
||||
<Card title="Recent Blocks">
|
||||
<div className="space-y-4">
|
||||
{blocks.map((block) => (
|
||||
<div
|
||||
key={block.hash}
|
||||
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
Block {formatNumber(block.number)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{truncateMiddle(block.hash)} · miner {truncateMiddle(block.miner)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{formatNumber(block.transaction_count)} tx · {relativeAge(block.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{blocks.length === 0 ? (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">No recent block data available.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Recent Transactions">
|
||||
<div className="space-y-4">
|
||||
{transactions.map((transaction) => (
|
||||
<div
|
||||
key={transaction.hash}
|
||||
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{truncateMiddle(transaction.hash, 12, 10)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Block {formatNumber(transaction.block_number)} · from {truncateMiddle(transaction.from_address)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusBadge
|
||||
status={transaction.status === 1 ? 'success' : 'failed'}
|
||||
tone={transaction.status === 1 ? 'normal' : 'danger'}
|
||||
/>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{relativeAge(transaction.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{transactions.length === 0 ? (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">No recent transaction data available.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</OperationsPageShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user