2026-04-07 23:22:12 -07:00
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
import { Card } from '@/libs/frontend-ui-primitives'
|
2026-04-10 12:52:17 -07:00
|
|
|
import Link from 'next/link'
|
2026-04-07 23:22:12 -07:00
|
|
|
import { explorerFeaturePages } from '@/data/explorerOperations'
|
|
|
|
|
import { blocksApi, type Block } from '@/services/api/blocks'
|
|
|
|
|
import {
|
|
|
|
|
missionControlApi,
|
|
|
|
|
type MissionControlBridgeStatusResponse,
|
|
|
|
|
type MissionControlChainStatus,
|
|
|
|
|
} from '@/services/api/missionControl'
|
2026-04-10 12:52:17 -07:00
|
|
|
import {
|
|
|
|
|
statsApi,
|
|
|
|
|
type ExplorerRecentActivitySnapshot,
|
|
|
|
|
type ExplorerStats,
|
|
|
|
|
type ExplorerTransactionTrendPoint,
|
|
|
|
|
} from '@/services/api/stats'
|
2026-04-07 23:22:12 -07:00
|
|
|
import { transactionsApi, type Transaction } from '@/services/api/transactions'
|
2026-04-10 12:52:17 -07:00
|
|
|
import { formatWeiAsEth } from '@/utils/format'
|
2026-04-07 23:22:12 -07:00
|
|
|
import OperationsPageShell, {
|
|
|
|
|
MetricCard,
|
|
|
|
|
StatusBadge,
|
|
|
|
|
formatNumber,
|
|
|
|
|
relativeAge,
|
|
|
|
|
truncateMiddle,
|
|
|
|
|
} from './OperationsPageShell'
|
|
|
|
|
|
2026-04-10 12:52:17 -07:00
|
|
|
interface AnalyticsOperationsPageProps {
|
|
|
|
|
initialStats?: ExplorerStats | null
|
|
|
|
|
initialTransactionTrend?: ExplorerTransactionTrendPoint[]
|
|
|
|
|
initialActivitySnapshot?: ExplorerRecentActivitySnapshot | null
|
|
|
|
|
initialBlocks?: Block[]
|
|
|
|
|
initialTransactions?: Transaction[]
|
|
|
|
|
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 23:22:12 -07:00
|
|
|
function getChainStatus(bridgeStatus: MissionControlBridgeStatusResponse | null): MissionControlChainStatus | null {
|
|
|
|
|
const chains = bridgeStatus?.data?.chains
|
|
|
|
|
if (!chains) return null
|
|
|
|
|
const [firstChain] = Object.values(chains)
|
|
|
|
|
return firstChain || null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:17 -07:00
|
|
|
export default function AnalyticsOperationsPage({
|
|
|
|
|
initialStats = null,
|
|
|
|
|
initialTransactionTrend = [],
|
|
|
|
|
initialActivitySnapshot = null,
|
|
|
|
|
initialBlocks = [],
|
|
|
|
|
initialTransactions = [],
|
|
|
|
|
initialBridgeStatus = null,
|
|
|
|
|
}: AnalyticsOperationsPageProps) {
|
|
|
|
|
const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
|
|
|
|
|
const [transactionTrend, setTransactionTrend] = useState<ExplorerTransactionTrendPoint[]>(initialTransactionTrend)
|
|
|
|
|
const [activitySnapshot, setActivitySnapshot] = useState<ExplorerRecentActivitySnapshot | null>(initialActivitySnapshot)
|
|
|
|
|
const [blocks, setBlocks] = useState<Block[]>(initialBlocks)
|
|
|
|
|
const [transactions, setTransactions] = useState<Transaction[]>(initialTransactions)
|
|
|
|
|
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
|
2026-04-07 23:22:12 -07:00
|
|
|
const [loadingError, setLoadingError] = useState<string | null>(null)
|
|
|
|
|
const page = explorerFeaturePages.analytics
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
2026-04-10 12:52:17 -07:00
|
|
|
const [statsResult, trendResult, snapshotResult, blocksResult, transactionsResult, bridgeResult] = await Promise.allSettled([
|
2026-04-07 23:22:12 -07:00
|
|
|
statsApi.get(),
|
2026-04-10 12:52:17 -07:00
|
|
|
statsApi.getTransactionTrend(),
|
|
|
|
|
statsApi.getRecentActivitySnapshot(),
|
2026-04-07 23:22:12 -07:00
|
|
|
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)
|
2026-04-10 12:52:17 -07:00
|
|
|
if (trendResult.status === 'fulfilled') setTransactionTrend(trendResult.value)
|
|
|
|
|
if (snapshotResult.status === 'fulfilled') setActivitySnapshot(snapshotResult.value)
|
2026-04-07 23:22:12 -07:00
|
|
|
if (blocksResult.status === 'fulfilled') setBlocks(blocksResult.value.data)
|
|
|
|
|
if (transactionsResult.status === 'fulfilled') setTransactions(transactionsResult.value.data)
|
|
|
|
|
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
|
|
|
|
|
|
2026-04-10 12:52:17 -07:00
|
|
|
const failedCount = [statsResult, trendResult, snapshotResult, blocksResult, transactionsResult, bridgeResult].filter(
|
2026-04-07 23:22:12 -07:00
|
|
|
(result) => result.status === 'rejected'
|
|
|
|
|
).length
|
|
|
|
|
|
2026-04-10 12:52:17 -07:00
|
|
|
if (failedCount === 6) {
|
2026-04-07 23:22:12 -07:00
|
|
|
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])
|
2026-04-10 12:52:17 -07:00
|
|
|
const trailingWindow = useMemo(() => transactionTrend.slice(0, 7), [transactionTrend])
|
|
|
|
|
const sevenDayAverage = useMemo(() => {
|
|
|
|
|
if (trailingWindow.length === 0) return 0
|
|
|
|
|
const total = trailingWindow.reduce((sum, point) => sum + point.transaction_count, 0)
|
|
|
|
|
return total / trailingWindow.length
|
|
|
|
|
}, [trailingWindow])
|
|
|
|
|
const topDay = useMemo(() => {
|
|
|
|
|
if (trailingWindow.length === 0) return null
|
|
|
|
|
return trailingWindow.reduce((best, point) => (point.transaction_count > best.transaction_count ? point : best))
|
|
|
|
|
}, [trailingWindow])
|
|
|
|
|
const averageGasUtilization = useMemo(() => {
|
|
|
|
|
if (blocks.length === 0) return 0
|
|
|
|
|
return blocks.reduce((sum, block) => {
|
|
|
|
|
const ratio = block.gas_limit > 0 ? block.gas_used / block.gas_limit : 0
|
|
|
|
|
return sum + ratio
|
|
|
|
|
}, 0) / blocks.length
|
|
|
|
|
}, [blocks])
|
|
|
|
|
const trendPeak = useMemo(
|
|
|
|
|
() => trailingWindow.reduce((max, point) => Math.max(max, point.transaction_count), 0),
|
|
|
|
|
[trailingWindow],
|
|
|
|
|
)
|
2026-04-07 23:22:12 -07:00
|
|
|
|
|
|
|
|
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.'
|
|
|
|
|
}
|
|
|
|
|
/>
|
2026-04-10 12:52:17 -07:00
|
|
|
<MetricCard
|
|
|
|
|
title="7d Avg Tx"
|
|
|
|
|
value={formatNumber(Math.round(sevenDayAverage))}
|
|
|
|
|
description="Average daily transactions over the latest seven charted days."
|
|
|
|
|
className="border border-violet-200 bg-violet-50/70 dark:border-violet-900/50 dark:bg-violet-950/20"
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
title="Recent Success Rate"
|
|
|
|
|
value={activitySnapshot ? `${Math.round(activitySnapshot.success_rate * 100)}%` : 'Unknown'}
|
|
|
|
|
description="Success rate across the public main-page transaction sample."
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
title="Failure Rate"
|
|
|
|
|
value={activitySnapshot ? `${Math.round(activitySnapshot.failure_rate * 100)}%` : 'Unknown'}
|
|
|
|
|
description="The complement to the recent success rate in the visible sample."
|
|
|
|
|
className="border border-rose-200 bg-rose-50/70 dark:border-rose-900/50 dark:bg-rose-950/20"
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
title="Avg Gas Used"
|
|
|
|
|
value={activitySnapshot ? formatNumber(Math.round(activitySnapshot.average_gas_used)) : 'Unknown'}
|
|
|
|
|
description="Average gas used in the recent sampled transactions."
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
title="Avg Block Gas"
|
|
|
|
|
value={`${Math.round(averageGasUtilization * 100)}%`}
|
|
|
|
|
description="Average gas utilization across the latest visible blocks."
|
|
|
|
|
className="border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20"
|
|
|
|
|
/>
|
2026-04-07 23:22:12 -07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
2026-04-10 12:52:17 -07:00
|
|
|
<Card title="Activity Trend">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
|
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Peak Day</div>
|
|
|
|
|
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
{topDay ? formatNumber(topDay.transaction_count) : 'Unknown'}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">{topDay?.date || 'No trend data yet'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Contract Creations</div>
|
|
|
|
|
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
{formatNumber(activitySnapshot?.contract_creations)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Within the sampled recent transaction feed.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Avg Sample Fee</div>
|
|
|
|
|
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
{activitySnapshot ? formatWeiAsEth(Math.round(activitySnapshot.average_fee_wei).toString(), 6) : 'Unknown'}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Average fee from the recent public transaction sample.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{activitySnapshot ? (
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
|
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Token Transfer Share</div>
|
|
|
|
|
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
{Math.round(activitySnapshot.token_transfer_share * 100)}%
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Sampled transactions involving token transfers.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Contract Call Share</div>
|
|
|
|
|
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
{Math.round(activitySnapshot.contract_call_share * 100)}%
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Sampled transactions calling contracts.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Creation Share</div>
|
|
|
|
|
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
{Math.round(activitySnapshot.contract_creation_share * 100)}%
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Sampled transactions deploying contracts.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{trailingWindow.map((point) => {
|
|
|
|
|
const width = trendPeak > 0 ? Math.max(8, Math.round((point.transaction_count / trendPeak) * 100)) : 0
|
|
|
|
|
return (
|
|
|
|
|
<div key={point.date}>
|
|
|
|
|
<div className="mb-1 flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
|
|
|
|
<span>{point.date}</span>
|
|
|
|
|
<span>{formatNumber(point.transaction_count)} tx</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="h-2 rounded-full bg-gray-200 dark:bg-gray-800">
|
|
|
|
|
<div className="h-2 rounded-full bg-primary-600" style={{ width: `${width}%` }} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
{trailingWindow.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Trend data is temporarily unavailable.</p>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-04-07 23:22:12 -07:00
|
|
|
<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>
|
2026-04-10 12:52:17 -07:00
|
|
|
<Link href={`/blocks/${block.number}`} className="text-base font-semibold text-primary-600 hover:underline">
|
2026-04-07 23:22:12 -07:00
|
|
|
Block {formatNumber(block.number)}
|
2026-04-10 12:52:17 -07:00
|
|
|
</Link>
|
2026-04-07 23:22:12 -07:00
|
|
|
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
2026-04-10 12:52:17 -07:00
|
|
|
{truncateMiddle(block.hash)} · miner{' '}
|
|
|
|
|
<Link href={`/addresses/${block.miner}`} className="text-primary-600 hover:underline">
|
|
|
|
|
{truncateMiddle(block.miner)}
|
|
|
|
|
</Link>
|
2026-04-07 23:22:12 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
2026-04-10 12:52:17 -07:00
|
|
|
{formatNumber(block.transaction_count)} tx · {Math.round((block.gas_limit > 0 ? block.gas_used / block.gas_limit : 0) * 100)}% gas · {relativeAge(block.timestamp)}
|
2026-04-07 23:22:12 -07:00
|
|
|
</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>
|
2026-04-10 12:52:17 -07:00
|
|
|
<Link href={`/transactions/${transaction.hash}`} className="text-base font-semibold text-primary-600 hover:underline">
|
2026-04-07 23:22:12 -07:00
|
|
|
{truncateMiddle(transaction.hash, 12, 10)}
|
2026-04-10 12:52:17 -07:00
|
|
|
</Link>
|
2026-04-07 23:22:12 -07:00
|
|
|
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
2026-04-10 12:52:17 -07:00
|
|
|
Block{' '}
|
|
|
|
|
<Link href={`/blocks/${transaction.block_number}`} className="text-primary-600 hover:underline">
|
|
|
|
|
{formatNumber(transaction.block_number)}
|
|
|
|
|
</Link>
|
|
|
|
|
{' '}· from{' '}
|
|
|
|
|
<Link href={`/addresses/${transaction.from_address}`} className="text-primary-600 hover:underline">
|
|
|
|
|
{truncateMiddle(transaction.from_address)}
|
|
|
|
|
</Link>
|
|
|
|
|
{transaction.to_address ? (
|
|
|
|
|
<>
|
|
|
|
|
{' '}· to{' '}
|
|
|
|
|
<Link href={`/addresses/${transaction.to_address}`} className="text-primary-600 hover:underline">
|
|
|
|
|
{truncateMiddle(transaction.to_address)}
|
|
|
|
|
</Link>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
2026-04-07 23:22:12 -07:00
|
|
|
</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>
|
2026-04-10 12:52:17 -07:00
|
|
|
<div className="mt-3 flex flex-wrap gap-2 text-xs">
|
|
|
|
|
{transaction.method ? <StatusBadge status={transaction.method} tone="warning" /> : null}
|
|
|
|
|
{transaction.contract_address ? <StatusBadge status="contract creation" tone="warning" /> : null}
|
|
|
|
|
{transaction.token_transfers && transaction.token_transfers.length > 0 ? (
|
|
|
|
|
<StatusBadge status={`${transaction.token_transfers.length} token transfer${transaction.token_transfers.length === 1 ? '' : 's'}`} />
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-04-07 23:22:12 -07:00
|
|
|
</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>
|
|
|
|
|
)
|
|
|
|
|
}
|