feat(explorer): API-driven CCIP route catalog on bridge page
Load destination bridge contracts from token-aggregation, add fallback polling, extend smoke tests, and document bridge routes client helper. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,9 @@ import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import { bridgeRoutesApi, normalizeBridgeRouteEntries, type BridgeRoutesResponse } from '@/services/api/bridgeRoutes'
|
||||
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
|
||||
import { HOME_DASHBOARD_REFRESH_MS } from '@/utils/featuredTokens'
|
||||
|
||||
type FeedState = 'connecting' | 'live' | 'fallback'
|
||||
|
||||
@@ -146,6 +149,7 @@ export default function BridgeMonitoringPage({
|
||||
}) {
|
||||
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
|
||||
const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
|
||||
const [bridgeRoutes, setBridgeRoutes] = useState<BridgeRoutesResponse | null>(null)
|
||||
const [feedState, setFeedState] = useState<FeedState>(initialBridgeStatus ? 'fallback' : 'connecting')
|
||||
const page = explorerFeaturePages.bridge
|
||||
|
||||
@@ -196,6 +200,49 @@ export default function BridgeMonitoringPage({
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
bridgeRoutesApi.getRoutesSafe().then(({ ok, data }) => {
|
||||
if (!cancelled && ok) {
|
||||
setBridgeRoutes(data)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (feedState !== 'fallback') return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const refreshSnapshot = async () => {
|
||||
try {
|
||||
const snapshot = await missionControlApi.getBridgeStatus()
|
||||
if (!cancelled) {
|
||||
setBridgeStatus(snapshot)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled && process.env.NODE_ENV !== 'production') {
|
||||
console.warn('Failed to refresh bridge monitoring snapshot:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createVisibilityAwarePoller({
|
||||
intervalMs: HOME_DASHBOARD_REFRESH_MS,
|
||||
task: refreshSnapshot,
|
||||
})
|
||||
}, [feedState])
|
||||
|
||||
const routeEntries = useMemo(
|
||||
() => normalizeBridgeRouteEntries(bridgeRoutes?.routes),
|
||||
[bridgeRoutes?.routes],
|
||||
)
|
||||
|
||||
const activityContext = useMemo(
|
||||
() =>
|
||||
summarizeChainActivity({
|
||||
@@ -407,6 +454,45 @@ export default function BridgeMonitoringPage({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{routeEntries.length > 0 ? (
|
||||
<Card title="CCIP route catalog" className="mb-8">
|
||||
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Destination bridge contracts from{' '}
|
||||
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">
|
||||
/token-aggregation/api/v1/bridge/routes
|
||||
</code>
|
||||
{bridgeRoutes?.source ? (
|
||||
<>
|
||||
{' '}
|
||||
(source: {bridgeRoutes.source}
|
||||
{bridgeRoutes.lastModified ? ` · updated ${relativeAge(bridgeRoutes.lastModified)}` : ''})
|
||||
</>
|
||||
) : null}
|
||||
.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
||||
<th className="py-2 pr-4">Bridge</th>
|
||||
<th className="py-2 pr-4">Destination</th>
|
||||
<th className="py-2">Contract</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{routeEntries.map((entry) => (
|
||||
<tr key={`${entry.bridge}-${entry.destination}`} className="border-b border-gray-100 last:border-0 dark:border-gray-800">
|
||||
<td className="py-2 pr-4 font-medium text-gray-900 dark:text-white">{entry.bridge}</td>
|
||||
<td className="py-2 pr-4 text-gray-700 dark:text-gray-300">{entry.destination}</td>
|
||||
<td className="py-2 font-mono text-xs text-gray-600 dark:text-gray-400">{entry.address}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user