- Introduced a new Diagnostics struct to capture transaction visibility state and activity state. - Updated BuildSnapshot function to return diagnostics alongside snapshot, completeness, and sampling. - Enhanced test cases to validate the new diagnostics data. - Updated frontend components to utilize the new diagnostics information for improved user feedback on freshness context. This change improves the observability of transaction activity and enhances the user experience by providing clearer insights into the freshness of data.
457 lines
18 KiB
TypeScript
457 lines
18 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { Card } from '@/libs/frontend-ui-primitives'
|
|
import { explorerFeaturePages } from '@/data/explorerOperations'
|
|
import {
|
|
routesApi,
|
|
type ExplorerNetwork,
|
|
type MissionControlLiquidityPool,
|
|
type RouteMatrixRoute,
|
|
type RouteMatrixResponse,
|
|
} from '@/services/api/routes'
|
|
import { missionControlApi, type MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
|
import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
|
import { summarizeChainActivity } from '@/utils/activityContext'
|
|
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
|
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
|
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
|
|
|
interface RoutesMonitoringPageProps {
|
|
initialRouteMatrix?: RouteMatrixResponse | null
|
|
initialNetworks?: ExplorerNetwork[]
|
|
initialPools?: MissionControlLiquidityPool[]
|
|
initialStats?: ExplorerStats | null
|
|
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
|
|
}
|
|
|
|
const canonicalLiquidityToken = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22'
|
|
|
|
function relativeAge(isoString?: string): string {
|
|
if (!isoString) return 'Unknown'
|
|
const parsed = Date.parse(isoString)
|
|
if (!Number.isFinite(parsed)) return 'Unknown'
|
|
const seconds = Math.max(0, Math.round((Date.now() - parsed) / 1000))
|
|
if (seconds < 60) return `${seconds}s ago`
|
|
const minutes = Math.round(seconds / 60)
|
|
if (minutes < 60) return `${minutes}m ago`
|
|
const hours = Math.round(minutes / 60)
|
|
return `${hours}h ago`
|
|
}
|
|
|
|
function compactAddress(value?: string): string {
|
|
if (!value) return 'Unspecified'
|
|
if (value.length <= 14) return value
|
|
return `${value.slice(0, 6)}...${value.slice(-4)}`
|
|
}
|
|
|
|
function formatUsd(value?: number): string {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) return 'Unknown'
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
maximumFractionDigits: 0,
|
|
}).format(value)
|
|
}
|
|
|
|
function protocolList(route: RouteMatrixRoute): string {
|
|
const protocols = Array.from(
|
|
new Set((route.legs || []).map((leg) => leg.protocol || leg.executor || '').filter(Boolean))
|
|
)
|
|
return protocols.length > 0 ? protocols.join(', ') : 'Unspecified'
|
|
}
|
|
|
|
function routeAssetPair(route: RouteMatrixRoute): string {
|
|
if (route.routeType === 'bridge') {
|
|
return route.assetSymbol || 'Bridge asset'
|
|
}
|
|
return [route.tokenInSymbol, route.tokenOutSymbol].filter(Boolean).join(' -> ') || 'Swap route'
|
|
}
|
|
|
|
function ActionLink({
|
|
href,
|
|
label,
|
|
external,
|
|
}: {
|
|
href: string
|
|
label: string
|
|
external?: boolean
|
|
}) {
|
|
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
|
|
const text = `${label} ->`
|
|
|
|
if (external) {
|
|
return (
|
|
<a href={href} className={className} target="_blank" rel="noopener noreferrer">
|
|
{text}
|
|
</a>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Link href={href} className={className}>
|
|
{text}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
export default function RoutesMonitoringPage({
|
|
initialRouteMatrix = null,
|
|
initialNetworks = [],
|
|
initialPools = [],
|
|
initialStats = null,
|
|
initialBridgeStatus = null,
|
|
}: RoutesMonitoringPageProps) {
|
|
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(initialRouteMatrix)
|
|
const [networks, setNetworks] = useState<ExplorerNetwork[]>(initialNetworks)
|
|
const [pools, setPools] = useState<MissionControlLiquidityPool[]>(initialPools)
|
|
const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
|
|
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
|
|
const [loadingError, setLoadingError] = useState<string | null>(null)
|
|
const page = explorerFeaturePages.routes
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
const load = async () => {
|
|
const [matrixResult, networksResult, poolsResult, statsResult, bridgeResult] = await Promise.allSettled([
|
|
routesApi.getRouteMatrix(),
|
|
routesApi.getNetworks(),
|
|
routesApi.getTokenPools(canonicalLiquidityToken),
|
|
statsApi.get(),
|
|
missionControlApi.getBridgeStatus(),
|
|
])
|
|
|
|
if (cancelled) return
|
|
|
|
if (matrixResult.status === 'fulfilled') {
|
|
setRouteMatrix(matrixResult.value)
|
|
}
|
|
if (networksResult.status === 'fulfilled') {
|
|
setNetworks(networksResult.value.networks || [])
|
|
}
|
|
if (poolsResult.status === 'fulfilled') {
|
|
setPools(poolsResult.value.pools || [])
|
|
}
|
|
if (statsResult.status === 'fulfilled') {
|
|
setStats(statsResult.value)
|
|
}
|
|
if (bridgeResult.status === 'fulfilled') {
|
|
setBridgeStatus(bridgeResult.value)
|
|
}
|
|
|
|
if (
|
|
matrixResult.status === 'rejected' &&
|
|
networksResult.status === 'rejected' &&
|
|
poolsResult.status === 'rejected' &&
|
|
statsResult.status === 'rejected' &&
|
|
bridgeResult.status === 'rejected'
|
|
) {
|
|
setLoadingError('Live route inventory is temporarily unavailable.')
|
|
}
|
|
}
|
|
|
|
load().catch((error) => {
|
|
if (!cancelled) {
|
|
setLoadingError(
|
|
error instanceof Error ? error.message : 'Live route inventory is temporarily unavailable.'
|
|
)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
const liveRoutes = useMemo(() => routeMatrix?.liveRoutes || [], [routeMatrix?.liveRoutes])
|
|
const plannedRoutes = useMemo(
|
|
() => routeMatrix?.blockedOrPlannedRoutes || [],
|
|
[routeMatrix?.blockedOrPlannedRoutes]
|
|
)
|
|
|
|
const familyCount = useMemo(() => {
|
|
return new Set(liveRoutes.flatMap((route) => route.aggregatorFamilies || [])).size
|
|
}, [liveRoutes])
|
|
|
|
const topRoutes = useMemo(() => {
|
|
const ordered = [...liveRoutes].sort((left, right) => {
|
|
if ((left.routeType || '') !== (right.routeType || '')) {
|
|
return left.routeType === 'bridge' ? -1 : 1
|
|
}
|
|
return (left.label || '').localeCompare(right.label || '')
|
|
})
|
|
return ordered.slice(0, 8)
|
|
}, [liveRoutes])
|
|
|
|
const highlightedNetworks = useMemo(() => {
|
|
return networks
|
|
.filter((network) => [138, 1, 651940, 56, 43114].includes(network.chainIdDecimal || 0))
|
|
.sort((left, right) => (left.chainIdDecimal || 0) - (right.chainIdDecimal || 0))
|
|
}, [networks])
|
|
const activityContext = useMemo(
|
|
() =>
|
|
summarizeChainActivity({
|
|
blocks: [],
|
|
transactions: [],
|
|
latestBlockNumber: stats?.latest_block,
|
|
latestBlockTimestamp: null,
|
|
freshness: resolveEffectiveFreshness(stats, bridgeStatus),
|
|
diagnostics: stats?.diagnostics ?? bridgeStatus?.data?.diagnostics ?? null,
|
|
}),
|
|
[bridgeStatus, stats],
|
|
)
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:py-8">
|
|
<div className="mb-6 max-w-4xl sm:mb-8">
|
|
<div className="mb-3 inline-flex rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-emerald-700">
|
|
{page.eyebrow}
|
|
</div>
|
|
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
|
|
{page.title}
|
|
</h1>
|
|
<p className="text-base leading-7 text-gray-600 dark:text-gray-400 sm:text-lg sm:leading-8">
|
|
{page.description}
|
|
</p>
|
|
</div>
|
|
|
|
{page.note ? (
|
|
<Card className="mb-6 border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20">
|
|
<p className="text-sm leading-6 text-amber-950 dark:text-amber-100">
|
|
{page.note}
|
|
</p>
|
|
</Card>
|
|
) : null}
|
|
|
|
{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">
|
|
<ActivityContextPanel context={activityContext} title="Routes Freshness Context" />
|
|
<FreshnessTrustNote
|
|
className="mt-3"
|
|
context={activityContext}
|
|
stats={stats}
|
|
bridgeStatus={bridgeStatus}
|
|
scopeLabel="Route availability reflects the current public route matrix and the same explorer freshness model used on the core explorer pages"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<Card className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20">
|
|
<div className="text-sm font-semibold uppercase tracking-wide text-emerald-800 dark:text-emerald-100">
|
|
Live Swap Routes
|
|
</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{routeMatrix?.counts?.liveSwapRoutes ?? liveRoutes.filter((route) => route.routeType === 'swap').length}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
Planner-visible same-chain routes on Chain 138.
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20">
|
|
<div className="text-sm font-semibold uppercase tracking-wide text-sky-800 dark:text-sky-100">
|
|
Live Bridge Routes
|
|
</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{routeMatrix?.counts?.liveBridgeRoutes ?? liveRoutes.filter((route) => route.routeType === 'bridge').length}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
Bridge routes exposed through the current route matrix.
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="border border-gray-200 dark:border-gray-700">
|
|
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
|
|
Network Catalog
|
|
</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{networks.length}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Published networks available through the explorer config surface.
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="border border-gray-200 dark:border-gray-700">
|
|
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
|
|
cUSDT Pool View
|
|
</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{pools.length}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Live mission-control pools for the canonical cUSDT token.
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="mb-8 grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
|
<Card title="Route Matrix Summary">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<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-sm text-gray-500 dark:text-gray-400">Generated</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{relativeAge(routeMatrix?.generatedAt)}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Matrix version {routeMatrix?.version || 'unknown'}
|
|
</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-sm text-gray-500 dark:text-gray-400">Updated Source</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{relativeAge(routeMatrix?.updated)}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{familyCount} partner families surfaced in live routes.
|
|
</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-sm text-gray-500 dark:text-gray-400">Filtered Live Routes</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{routeMatrix?.counts?.filteredLiveRoutes ?? liveRoutes.length}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Includes swap and bridge lanes currently in the public matrix.
|
|
</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-sm text-gray-500 dark:text-gray-400">Planned / Blocked</div>
|
|
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{routeMatrix?.counts?.blockedOrPlannedRoutes ?? plannedRoutes.length}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Remaining lanes still waiting on pools, funding, or routing support.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="Highlighted Networks">
|
|
<div className="space-y-3">
|
|
{highlightedNetworks.map((network) => (
|
|
<div
|
|
key={`${network.chainIdDecimal}-${network.chainName}`}
|
|
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
|
>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{network.chainName || 'Unknown chain'}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Chain ID {network.chainIdDecimal ?? 'Unknown'} · {network.shortName || 'n/a'}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="mb-8 grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
|
<Card title="Live Route Snapshot">
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
{topRoutes.map((route) => (
|
|
<div
|
|
key={route.routeId}
|
|
className="rounded-2xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{route.label || route.routeId}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{routeAssetPair(route)}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-full bg-primary-50 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">
|
|
{route.routeType || 'route'}
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
|
|
{`${route.fromChainId} -> ${route.toChainId} · ${route.hopCount ?? 0} hop${
|
|
(route.hopCount ?? 0) === 1 ? '' : 's'
|
|
}`}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Protocols: {protocolList(route)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="cUSDT Mission-Control Pools">
|
|
<div className="space-y-4">
|
|
{pools.map((pool) => (
|
|
<div
|
|
key={pool.address}
|
|
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
|
>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{(pool.token0?.symbol || '?') + ' / ' + (pool.token1?.symbol || '?')}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{pool.dex || 'Unknown DEX'} · TVL {formatUsd(pool.tvl)}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Pool {compactAddress(pool.address)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="mb-8">
|
|
<Card title="Planned Route Backlog">
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
{plannedRoutes.slice(0, 6).map((route) => (
|
|
<div
|
|
key={route.routeId}
|
|
className="rounded-2xl border border-amber-200 bg-amber-50/70 p-4 dark:border-amber-900/50 dark:bg-amber-950/20"
|
|
>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{route.routeId}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
{(route.tokenInSymbols || []).join(' / ') || routeAssetPair(route)}
|
|
</div>
|
|
<div className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
|
{route.reason || 'Pending additional deployment or routing work.'}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
{page.actions.map((action) => (
|
|
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
|
|
<div className="flex h-full flex-col">
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{action.title}
|
|
</div>
|
|
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
|
{action.description}
|
|
</p>
|
|
<div className="mt-4">
|
|
<ActionLink
|
|
href={action.href}
|
|
label={action.label}
|
|
external={Boolean((action as { external?: boolean }).external)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|