feat(explorer): API-driven CCIP route catalog on bridge page
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 14s
Validate Explorer / frontend (push) Successful in 1m35s
Validate Explorer / smoke-e2e (push) Failing after 1m38s

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:
defiQUG
2026-05-22 21:12:21 -07:00
parent 6a64d2fec6
commit 847cfeb48b
4 changed files with 176 additions and 0 deletions

View File

@@ -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">

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { normalizeBridgeRouteEntries, type BridgeRouteCatalog } from './bridgeRoutes'
describe('bridgeRoutesApi helpers', () => {
it('normalizes WETH9 and WETH10 route tables', () => {
const routes: BridgeRouteCatalog = {
weth9: {
'Ethereum Mainnet (1)': '0x1111111111111111111111111111111111111111',
},
weth10: {
'Base (8453)': '0x2222222222222222222222222222222222222222',
},
}
expect(normalizeBridgeRouteEntries(routes)).toEqual([
{
bridge: 'WETH10',
destination: 'Base (8453)',
address: '0x2222222222222222222222222222222222222222',
},
{
bridge: 'WETH9',
destination: 'Ethereum Mainnet (1)',
address: '0x1111111111111111111111111111111111111111',
},
])
})
})

View File

@@ -0,0 +1,55 @@
import { getExplorerApiBase } from './blockscout'
export interface BridgeRouteCatalog {
weth9: Record<string, string>
weth10: Record<string, string>
trustless?: Record<string, string>
}
export interface BridgeRoutesResponse {
source?: string
lastModified?: string
updated?: string
description?: string
routes: BridgeRouteCatalog
chain138Bridges?: Record<string, string>
}
const bridgeRoutesBase = `${getExplorerApiBase()}/token-aggregation/api/v1/bridge`
export function normalizeBridgeRouteEntries(
routes: BridgeRouteCatalog | null | undefined,
): Array<{ bridge: 'WETH9' | 'WETH10'; destination: string; address: string }> {
if (!routes) return []
const entries: Array<{ bridge: 'WETH9' | 'WETH10'; destination: string; address: string }> = []
for (const [destination, address] of Object.entries(routes.weth9 || {})) {
if (address) entries.push({ bridge: 'WETH9', destination, address })
}
for (const [destination, address] of Object.entries(routes.weth10 || {})) {
if (address) entries.push({ bridge: 'WETH10', destination, address })
}
return entries.sort((left, right) =>
left.destination.localeCompare(right.destination) || left.bridge.localeCompare(right.bridge),
)
}
export const bridgeRoutesApi = {
getRoutesSafe: async (): Promise<{ ok: boolean; data: BridgeRoutesResponse | null }> => {
try {
const response = await fetch(`${bridgeRoutesBase}/routes`)
if (!response.ok) {
return { ok: false, data: null }
}
const payload = (await response.json()) as BridgeRoutesResponse
if (!payload?.routes?.weth9 || !payload?.routes?.weth10) {
return { ok: false, data: null }
}
return { ok: true, data: payload }
} catch {
return { ok: false, data: null }
}
},
}

View File

@@ -31,4 +31,10 @@ test.describe('Explorer sprint smoke', () => {
await expect(page.getByRole('heading', { name: /Operations Hub/i })).toBeVisible({ timeout: 10000 })
await expect(page.getByText(/Extended Metamask dual-chain catalog/i).first()).toBeVisible({ timeout: 10000 })
})
test('bridge page loads CCIP route catalog', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/bridge`, { waitUntil: 'domcontentloaded', timeout: 30000 })
await expect(page.getByRole('heading', { name: /Bridge & Relay Monitoring/i })).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/CCIP route catalog/i).first()).toBeVisible({ timeout: 15000 })
})
})