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:
301
frontend/src/components/explorer/MoreOperationsPage.tsx
Normal file
301
frontend/src/components/explorer/MoreOperationsPage.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { explorerFeaturePages } from '@/data/explorerOperations'
|
||||
import { configApi, type CapabilitiesResponse, type NetworksConfigResponse, type TokenListResponse } from '@/services/api/config'
|
||||
import { getMissionControlRelays, missionControlApi, type MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
||||
import { routesApi, type RouteMatrixResponse } from '@/services/api/routes'
|
||||
|
||||
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 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 MoreOperationsPage() {
|
||||
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
|
||||
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
|
||||
const [networksConfig, setNetworksConfig] = useState<NetworksConfigResponse | null>(null)
|
||||
const [tokenList, setTokenList] = useState<TokenListResponse | null>(null)
|
||||
const [capabilities, setCapabilities] = useState<CapabilitiesResponse | null>(null)
|
||||
const [loadingError, setLoadingError] = useState<string | null>(null)
|
||||
const page = explorerFeaturePages.more
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const load = async () => {
|
||||
const [bridgeResult, routesResult, networksResult, tokenListResult, capabilitiesResult] =
|
||||
await Promise.allSettled([
|
||||
missionControlApi.getBridgeStatus(),
|
||||
routesApi.getRouteMatrix(),
|
||||
configApi.getNetworks(),
|
||||
configApi.getTokenList(),
|
||||
configApi.getCapabilities(),
|
||||
])
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
|
||||
if (routesResult.status === 'fulfilled') setRouteMatrix(routesResult.value)
|
||||
if (networksResult.status === 'fulfilled') setNetworksConfig(networksResult.value)
|
||||
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
|
||||
if (capabilitiesResult.status === 'fulfilled') setCapabilities(capabilitiesResult.value)
|
||||
|
||||
const failedCount = [
|
||||
bridgeResult,
|
||||
routesResult,
|
||||
networksResult,
|
||||
tokenListResult,
|
||||
capabilitiesResult,
|
||||
].filter((result) => result.status === 'rejected').length
|
||||
|
||||
if (failedCount === 5) {
|
||||
setLoadingError('Public explorer operations data is temporarily unavailable.')
|
||||
}
|
||||
}
|
||||
|
||||
load().catch((error) => {
|
||||
if (!cancelled) {
|
||||
setLoadingError(
|
||||
error instanceof Error ? error.message : 'Public explorer operations data is temporarily unavailable.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const relayCount = useMemo(() => {
|
||||
const relays = getMissionControlRelays(bridgeStatus)
|
||||
return relays ? Object.keys(relays).length : 0
|
||||
}, [bridgeStatus])
|
||||
|
||||
const totalQueue = useMemo(() => {
|
||||
const relays = getMissionControlRelays(bridgeStatus)
|
||||
if (!relays) return 0
|
||||
return Object.values(relays).reduce((sum, relay) => {
|
||||
const queueSize = relay.url_probe?.body?.queue?.size ?? relay.file_snapshot?.queue?.size ?? 0
|
||||
return sum + queueSize
|
||||
}, 0)
|
||||
}, [bridgeStatus])
|
||||
|
||||
const tokenChainCoverage = useMemo(() => {
|
||||
return new Set((tokenList?.tokens || []).map((token) => token.chainId).filter(Boolean)).size
|
||||
}, [tokenList])
|
||||
|
||||
const topSymbols = useMemo(() => {
|
||||
return Array.from(
|
||||
new Set((tokenList?.tokens || []).map((token) => token.symbol).filter(Boolean) as string[])
|
||||
).slice(0, 8)
|
||||
}, [tokenList])
|
||||
|
||||
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-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-violet-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 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<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">
|
||||
Bridge Fleet
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{bridgeStatus?.data?.status || 'unknown'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{relayCount} managed lanes · queue {totalQueue}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<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">
|
||||
Route Coverage
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{routeMatrix?.counts?.filteredLiveRoutes ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{routeMatrix?.counts?.liveSwapRoutes ?? 0} swaps · {routeMatrix?.counts?.liveBridgeRoutes ?? 0} bridges
|
||||
</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">
|
||||
Wallet Surface
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{networksConfig?.chains?.length ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Chains · {(tokenList?.tokens || []).length} tokens across {tokenChainCoverage} networks
|
||||
</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">
|
||||
RPC Capabilities
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{capabilities?.http?.supportedMethods?.length ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
HTTP methods · {capabilities?.tracing?.supportedMethods?.length ?? 0} tracing methods
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
||||
<Card title="Operations Snapshot">
|
||||
<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">Bridge checked</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{relativeAge(bridgeStatus?.data?.checked_at)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Public mission-control snapshot freshness.
|
||||
</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">Route matrix updated</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">
|
||||
Token-aggregation route inventory timestamp.
|
||||
</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">Default chain</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{networksConfig?.defaultChainId ?? 'Unknown'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Wallet onboarding points at Chain 138 by default.
|
||||
</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">Wallet support</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{capabilities?.walletSupport?.walletAddEthereumChain && capabilities?.walletSupport?.walletWatchAsset
|
||||
? 'Ready'
|
||||
: 'Partial'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
`wallet_addEthereumChain` and `wallet_watchAsset` compatibility.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Public Config Highlights">
|
||||
<div className="space-y-4">
|
||||
<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">Featured symbols</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{topSymbols.map((symbol) => (
|
||||
<span
|
||||
key={symbol}
|
||||
className="rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
||||
>
|
||||
{symbol}
|
||||
</span>
|
||||
))}
|
||||
</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">Tracing posture</div>
|
||||
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
Supported: {(capabilities?.tracing?.supportedMethods || []).join(', ') || 'None'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Unsupported: {(capabilities?.tracing?.unsupportedMethods || []).join(', ') || 'None'}
|
||||
</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={'external' in action ? action.external : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user