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:
defiQUG
2026-04-07 23:22:12 -07:00
parent 4044fb07e1
commit bdae5a9f6e
224 changed files with 19671 additions and 3291 deletions

View File

@@ -1,48 +1 @@
import { useState } from 'react'
import clsx from 'clsx'
interface AddressProps {
address: string
chainId?: number
showCopy?: boolean
showENS?: boolean
truncate?: boolean
className?: string
}
export function Address({
address,
chainId,
showCopy = true,
showENS = false,
truncate = false,
className,
}: AddressProps) {
const [copied, setCopied] = useState(false)
const displayAddress = truncate
? `${address.slice(0, 6)}...${address.slice(-4)}`
: address
const handleCopy = async () => {
await navigator.clipboard.writeText(address)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className={clsx('flex items-center gap-2', className)}>
<span className="font-mono text-sm">{displayAddress}</span>
{showCopy && (
<button
onClick={handleCopy}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
title="Copy address"
>
{copied ? '✓' : '📋'}
</button>
)}
</div>
)
}
export { Address } from '@/libs/frontend-ui-primitives/Address'

View File

@@ -1,37 +1 @@
import { ButtonHTMLAttributes, ReactNode } from 'react'
import clsx from 'clsx'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
children: ReactNode
}
export function Button({
variant = 'primary',
size = 'md',
className,
children,
...props
}: ButtonProps) {
return (
<button
className={clsx(
'font-medium rounded-lg transition-colors',
{
'bg-primary-600 text-white hover:bg-primary-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
)}
{...props}
>
{children}
</button>
)
}
export { Button } from '@/libs/frontend-ui-primitives/Button'

View File

@@ -1,27 +1 @@
import { ReactNode } from 'react'
import clsx from 'clsx'
interface CardProps {
children: ReactNode
className?: string
title?: string
}
export function Card({ children, className, title }: CardProps) {
return (
<div
className={clsx(
'bg-white dark:bg-gray-800 rounded-lg shadow-md p-6',
className
)}
>
{title && (
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
{title}
</h3>
)}
{children}
</div>
)
}
export { Card } from '@/libs/frontend-ui-primitives/Card'

View File

@@ -0,0 +1,34 @@
import type { ReactNode } from 'react'
import clsx from 'clsx'
interface DetailRowProps {
label: string
children: ReactNode
className?: string
labelClassName?: string
valueClassName?: string
}
export function DetailRow({
label,
children,
className,
labelClassName,
valueClassName,
}: DetailRowProps) {
return (
<div className={clsx('flex flex-col gap-1.5 sm:flex-row sm:items-start sm:gap-4', className)}>
<dt
className={clsx(
'text-sm font-semibold text-gray-700 dark:text-gray-300 sm:w-36 sm:shrink-0',
labelClassName,
)}
>
{label}
</dt>
<dd className={clsx('min-w-0 text-sm text-gray-900 dark:text-gray-100', valueClassName)}>
{children}
</dd>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import type { ReactNode } from 'react'
import Navbar from './Navbar'
import Footer from './Footer'
export default function ExplorerChrome({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Navbar />
<div className="flex-1">{children}</div>
<Footer />
</div>
)
}

View File

@@ -8,10 +8,10 @@ export default function Footer() {
return (
<footer className="mt-auto border-t border-gray-200 dark:border-gray-700 bg-white/90 dark:bg-gray-900/90 backdrop-blur">
<div className="container mx-auto px-4 py-8">
<div className="grid gap-6 md:grid-cols-[1.5fr_1fr_1fr]">
<div className="space-y-3">
<div className="text-lg font-semibold text-gray-900 dark:text-white">
<div className="container mx-auto px-4 py-6 sm:py-8">
<div className="grid gap-4 sm:gap-6 md:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,1fr)]">
<div className="space-y-3 rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
SolaceScanScout
</div>
<p className="max-w-xl text-sm leading-6 text-gray-600 dark:text-gray-400">
@@ -24,13 +24,16 @@ export default function Footer() {
</p>
</div>
<div>
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Resources
</div>
<ul className="space-y-2 text-sm">
<li><a className={footerLinkClass} href="/docs.html">Documentation</a></li>
<li><Link className={footerLinkClass} href="/bridge">Bridge Monitoring</Link></li>
<li><Link className={footerLinkClass} href="/liquidity">Liquidity Access</Link></li>
<li><Link className={footerLinkClass} href="/routes">Routes</Link></li>
<li><Link className={footerLinkClass} href="/more">More Tools</Link></li>
<li><Link className={footerLinkClass} href="/addresses">Addresses</Link></li>
<li><Link className={footerLinkClass} href="/watchlist">Watchlist</Link></li>
<li><a className={footerLinkClass} href="/privacy.html">Privacy Policy</a></li>
@@ -39,7 +42,7 @@ export default function Footer() {
</ul>
</div>
<div>
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Contact
</div>
@@ -56,6 +59,12 @@ export default function Footer() {
explorer.d-bis.org/snap/
</a>
</p>
<p>
Command center:{' '}
<a className={footerLinkClass} href="/chain138-command-center.html" target="_blank" rel="noopener noreferrer">
Chain 138 visual map
</a>
</p>
<p className="text-xs leading-5 text-gray-500 dark:text-gray-500">
Questions about the explorer, chain metadata, route discovery, or liquidity access
can be sent to the support mailbox above.

View File

@@ -86,26 +86,42 @@ export default function Navbar() {
const [exploreOpen, setExploreOpen] = useState(false)
const [toolsOpen, setToolsOpen] = useState(false)
const toggleMobileMenu = () => {
setMobileMenuOpen((open) => {
const nextOpen = !open
if (!nextOpen) {
setExploreOpen(false)
setToolsOpen(false)
}
return nextOpen
})
}
return (
<nav className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-4 md:gap-8">
<div className="flex h-14 items-center justify-between sm:h-16">
<div className="flex min-w-0 items-center gap-3 md:gap-8">
<Link
href="/"
className="group inline-flex flex-col rounded-xl px-3 py-2 text-xl font-bold text-primary-600 transition-colors hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-gray-700/70"
className="group inline-flex max-w-[calc(100vw-5rem)] flex-col rounded-xl px-2 py-1.5 text-base font-bold text-primary-600 transition-colors hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-gray-700/70 sm:max-w-none sm:px-3 sm:py-2 sm:text-xl"
onClick={() => setMobileMenuOpen(false)}
aria-label="Go to explorer home"
>
<span className="flex items-center gap-2">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-primary-600 text-white shadow-sm transition-transform group-hover:-translate-y-0.5 dark:bg-primary-500">
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
<span className="flex min-w-0 items-center gap-2">
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary-600 text-white shadow-sm transition-transform group-hover:-translate-y-0.5 dark:bg-primary-500 sm:h-8 sm:w-8">
<svg className="h-3.5 w-3.5 sm:h-4 sm:w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
<path d="M12 2.5 3.5 6.5v11l8.5 4 8.5-4v-11L12 2.5Zm0 2.24 6.44 3.03L12 10.8 5.56 7.77 12 4.74Zm-7 4.63L11 13.1v6.07L5 16.4V9.37Zm9 9.8v-6.07l6-2.92v6.03l-6 2.96Z" />
</svg>
</span>
<span>SolaceScanScout</span>
<span className="min-w-0 truncate">
<span className="sm:hidden">SolaceScan</span>
<span className="hidden sm:inline">SolaceScanScout</span>
</span>
</span>
<span className="mt-0.5 hidden text-xs font-normal text-gray-500 transition-colors group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200 sm:block">
The Defi Oracle Meta Explorer
</span>
<span className="mt-0.5 text-xs font-normal text-gray-500 transition-colors group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200">The Defi Oracle Meta Explorer</span>
</Link>
<div className="hidden md:flex items-center gap-1">
<NavDropdown
@@ -117,6 +133,12 @@ export default function Navbar() {
<DropdownItem href="/transactions" icon={<span className="text-gray-400"></span>}>Transactions</DropdownItem>
<DropdownItem href="/addresses" icon={<span className="text-gray-400"></span>}>Addresses</DropdownItem>
</NavDropdown>
<Link
href="/wallet"
className={`hidden md:inline-flex items-center px-3 py-2 rounded-md ${pathname.startsWith('/wallet') ? navLinkActive : navLink}`}
>
Wallet
</Link>
<NavDropdown
label="Tools"
icon={<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>}
@@ -127,6 +149,10 @@ export default function Navbar() {
<DropdownItem href="/watchlist">Watchlist</DropdownItem>
<DropdownItem href="/wallet">Wallet</DropdownItem>
<DropdownItem href="/liquidity">Liquidity</DropdownItem>
<DropdownItem href="/bridge">Bridge</DropdownItem>
<DropdownItem href="/routes">Routes</DropdownItem>
<DropdownItem href="/more">More</DropdownItem>
<DropdownItem href="/chain138-command-center.html" external>Command Center</DropdownItem>
</NavDropdown>
</div>
</div>
@@ -134,7 +160,7 @@ export default function Navbar() {
<button
type="button"
className="p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={() => setMobileMenuOpen((o) => !o)}
onClick={toggleMobileMenu}
aria-expanded={mobileMenuOpen}
aria-label="Toggle menu"
>
@@ -147,7 +173,7 @@ export default function Navbar() {
</div>
</div>
{mobileMenuOpen && (
<div className="md:hidden py-3 border-t border-gray-200 dark:border-gray-700">
<div className="border-t border-gray-200 py-2 pb-3 dark:border-gray-700 md:hidden">
<div className="flex flex-col gap-1">
<Link href="/" className={`px-3 py-2.5 rounded-md ${pathname === '/' ? navLinkActive : navLink}`} onClick={() => setMobileMenuOpen(false)}>Home</Link>
<div className="relative">
@@ -176,6 +202,10 @@ export default function Navbar() {
<li><Link href="/watchlist" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Watchlist</Link></li>
<li><Link href="/wallet" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Wallet</Link></li>
<li><Link href="/liquidity" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Liquidity</Link></li>
<li><Link href="/bridge" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Bridge</Link></li>
<li><Link href="/routes" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Routes</Link></li>
<li><Link href="/more" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>More</Link></li>
<li><a href="/chain138-command-center.html" className={`block px-3 py-2 rounded-md ${navLink}`} target="_blank" rel="noopener noreferrer" onClick={() => setMobileMenuOpen(false)}>Command Center</a></li>
</ul>
)}
</div>

View File

@@ -1,58 +1 @@
import { ReactNode } from 'react'
import clsx from 'clsx'
interface Column<T> {
header: string
accessor: (row: T) => ReactNode
className?: string
}
interface TableProps<T> {
columns: Column<T>[]
data: T[]
className?: string
/** Stable key for each row (e.g. row => row.id or row => row.hash). Falls back to index if not provided. */
keyExtractor?: (row: T) => string | number
}
export function Table<T>({ columns, data, className, keyExtractor }: TableProps<T>) {
return (
<div className={clsx('overflow-x-auto', className)}>
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
{columns.map((column, index) => (
<th
key={index}
className={clsx(
'px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider',
column.className
)}
>
{column.header}
</th>
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{data.map((row, rowIndex) => (
<tr key={keyExtractor ? keyExtractor(row) : rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
{columns.map((column, colIndex) => (
<td
key={colIndex}
className={clsx(
'px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100',
column.className
)}
>
{column.accessor(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
export { Table } from '@/libs/frontend-ui-primitives/Table'

View File

@@ -0,0 +1,177 @@
import { useEffect, useMemo, useState } from 'react'
import { Card } from '@/libs/frontend-ui-primitives'
import { explorerFeaturePages } from '@/data/explorerOperations'
import { blocksApi, type Block } from '@/services/api/blocks'
import {
missionControlApi,
type MissionControlBridgeStatusResponse,
type MissionControlChainStatus,
} from '@/services/api/missionControl'
import { statsApi, type ExplorerStats } from '@/services/api/stats'
import { transactionsApi, type Transaction } from '@/services/api/transactions'
import OperationsPageShell, {
MetricCard,
StatusBadge,
formatNumber,
relativeAge,
truncateMiddle,
} from './OperationsPageShell'
function getChainStatus(bridgeStatus: MissionControlBridgeStatusResponse | null): MissionControlChainStatus | null {
const chains = bridgeStatus?.data?.chains
if (!chains) return null
const [firstChain] = Object.values(chains)
return firstChain || null
}
export default function AnalyticsOperationsPage() {
const [stats, setStats] = useState<ExplorerStats | null>(null)
const [blocks, setBlocks] = useState<Block[]>([])
const [transactions, setTransactions] = useState<Transaction[]>([])
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
const [loadingError, setLoadingError] = useState<string | null>(null)
const page = explorerFeaturePages.analytics
useEffect(() => {
let cancelled = false
const load = async () => {
const [statsResult, blocksResult, transactionsResult, bridgeResult] = await Promise.allSettled([
statsApi.get(),
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)
if (blocksResult.status === 'fulfilled') setBlocks(blocksResult.value.data)
if (transactionsResult.status === 'fulfilled') setTransactions(transactionsResult.value.data)
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
const failedCount = [statsResult, blocksResult, transactionsResult, bridgeResult].filter(
(result) => result.status === 'rejected'
).length
if (failedCount === 4) {
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])
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.'
}
/>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<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>
<div className="text-base font-semibold text-gray-900 dark:text-white">
Block {formatNumber(block.number)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{truncateMiddle(block.hash)} · miner {truncateMiddle(block.miner)}
</div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{formatNumber(block.transaction_count)} tx · {relativeAge(block.timestamp)}
</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>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{truncateMiddle(transaction.hash, 12, 10)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Block {formatNumber(transaction.block_number)} · from {truncateMiddle(transaction.from_address)}
</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>
</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>
)
}

View File

@@ -0,0 +1,326 @@
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import {
getMissionControlRelayLabel,
getMissionControlRelays,
missionControlApi,
type MissionControlBridgeStatusResponse,
type MissionControlRelayPayload,
type MissionControlRelaySnapshot,
} from '@/services/api/missionControl'
import { explorerFeaturePages } from '@/data/explorerOperations'
type FeedState = 'connecting' | 'live' | 'fallback'
interface RelayLaneCard {
key: string
label: string
status: string
profile: string
sourceChain: string
destinationChain: string
queueSize: number
processed: number
failed: number
lastPolled: string
bridgeAddress: string
}
const relayOrder = ['mainnet_cw', 'mainnet_weth', 'bsc', 'avax', 'avax_cw', 'avax_to_138']
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 shortAddress(value?: string): string {
if (!value) return 'Unspecified'
if (value.length <= 14) return value
return `${value.slice(0, 6)}...${value.slice(-4)}`
}
function resolveSnapshot(relay?: MissionControlRelayPayload): MissionControlRelaySnapshot | null {
return relay?.url_probe?.body || relay?.file_snapshot || null
}
function laneToneClasses(status: string): string {
const normalized = status.toLowerCase()
if (['degraded', 'stale', 'stopped', 'down', 'snapshot-error'].includes(normalized)) {
return 'border-red-200 bg-red-50/80 dark:border-red-900/60 dark:bg-red-950/20'
}
if (['paused', 'starting'].includes(normalized)) {
return 'border-amber-200 bg-amber-50/80 dark:border-amber-900/60 dark:bg-amber-950/20'
}
return 'border-emerald-200 bg-emerald-50/80 dark:border-emerald-900/60 dark:bg-emerald-950/20'
}
function statusPillClasses(status: string): string {
const normalized = status.toLowerCase()
if (['degraded', 'stale', 'stopped', 'down', 'snapshot-error'].includes(normalized)) {
return 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-100'
}
if (['paused', 'starting'].includes(normalized)) {
return 'bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-100'
}
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-100'
}
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 BridgeMonitoringPage() {
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
const [feedState, setFeedState] = useState<FeedState>('connecting')
const page = explorerFeaturePages.bridge
useEffect(() => {
let cancelled = false
const loadSnapshot = async () => {
try {
const snapshot = await missionControlApi.getBridgeStatus()
if (!cancelled) {
setBridgeStatus(snapshot)
}
} catch (error) {
if (!cancelled && process.env.NODE_ENV !== 'production') {
console.warn('Failed to load bridge monitoring snapshot:', error)
}
}
}
loadSnapshot()
const unsubscribe = missionControlApi.subscribeBridgeStatus(
(status) => {
if (!cancelled) {
setBridgeStatus(status)
setFeedState('live')
}
},
(error) => {
if (!cancelled) {
setFeedState('fallback')
}
if (process.env.NODE_ENV !== 'production') {
console.warn('Bridge monitoring live stream issue:', error)
}
}
)
return () => {
cancelled = true
unsubscribe()
}
}, [])
const relayLanes = useMemo((): RelayLaneCard[] => {
const relays = getMissionControlRelays(bridgeStatus)
if (!relays) return []
const orderIndex = new Map(relayOrder.map((key, index) => [key, index]))
return Object.entries(relays)
.map(([key, relay]) => {
const snapshot = resolveSnapshot(relay)
const status = String(snapshot?.status || (relay.file_snapshot_error ? 'snapshot-error' : 'configured')).toLowerCase()
return {
key,
label: getMissionControlRelayLabel(key),
status,
profile: snapshot?.service?.profile || key,
sourceChain: snapshot?.source?.chain_name || 'Unknown',
destinationChain: snapshot?.destination?.chain_name || 'Unknown',
queueSize: snapshot?.queue?.size ?? 0,
processed: snapshot?.queue?.processed ?? 0,
failed: snapshot?.queue?.failed ?? 0,
lastPolled: relativeAge(snapshot?.last_source_poll?.at),
bridgeAddress:
snapshot?.destination?.relay_bridge_default ||
snapshot?.destination?.relay_bridge ||
snapshot?.source?.bridge_filter ||
'',
}
})
.sort((left, right) => {
const leftIndex = orderIndex.get(left.key) ?? Number.MAX_SAFE_INTEGER
const rightIndex = orderIndex.get(right.key) ?? Number.MAX_SAFE_INTEGER
return leftIndex - rightIndex || left.label.localeCompare(right.label)
})
}, [bridgeStatus])
const chainStatus = bridgeStatus?.data?.chains?.['138']
const overallStatus = bridgeStatus?.data?.status || 'unknown'
const checkedAt = relativeAge(bridgeStatus?.data?.checked_at)
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-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-sky-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}
<div className="mb-6 grid gap-4 lg:grid-cols-3">
<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">
Relay Fleet
</div>
<div className="mt-2 text-2xl font-bold text-gray-900 dark:text-white">
{overallStatus}
</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
{relayLanes.length} managed lanes visible
</div>
<div className="mt-2 text-xs font-medium uppercase tracking-wide text-sky-800/80 dark:text-sky-100/80">
Feed: {feedState === 'live' ? 'Live SSE' : feedState === 'fallback' ? 'Snapshot fallback' : 'Connecting'}
</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">
Chain 138 RPC
</div>
<div className="mt-2 text-2xl font-bold text-gray-900 dark:text-white">
{chainStatus?.status || 'unknown'}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Head age: {chainStatus?.head_age_sec != null ? `${chainStatus.head_age_sec.toFixed(1)}s` : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Latency: {chainStatus?.latency_ms != null ? `${chainStatus.latency_ms}ms` : 'Unknown'}
</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">
Last Check
</div>
<div className="mt-2 text-2xl font-bold text-gray-900 dark:text-white">
{checkedAt}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Public status JSON and live stream are both active.
</div>
<div className="mt-4">
<ActionLink href="/explorer-api/v1/track1/bridge/status" label="Open status JSON" external />
</div>
</Card>
</div>
<div className="mb-8 grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
{relayLanes.map((lane) => (
<Card key={lane.key} className={`border ${laneToneClasses(lane.status)}`}>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{lane.label}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{`${lane.sourceChain} -> ${lane.destinationChain}`}
</div>
</div>
<div className={`rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide ${statusPillClasses(lane.status)}`}>
{lane.status}
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-gray-500 dark:text-gray-400">Profile</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.profile}</div>
</div>
<div>
<div className="text-gray-500 dark:text-gray-400">Queue</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.queueSize}</div>
</div>
<div>
<div className="text-gray-500 dark:text-gray-400">Processed</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.processed}</div>
</div>
<div>
<div className="text-gray-500 dark:text-gray-400">Failed</div>
<div className="font-medium text-gray-900 dark:text-white">{lane.failed}</div>
</div>
</div>
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
Last polled: {lane.lastPolled}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Bridge: {shortAddress(lane.bridgeAddress)}
</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>
)
}

View File

@@ -0,0 +1,66 @@
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import type { ExplorerFeatureAction, ExplorerFeaturePage } from '@/data/explorerOperations'
function ActionLink({ action }: { action: ExplorerFeatureAction }) {
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
const label = `${action.label} ->`
if (action.external) {
return (
<a href={action.href} className={className} target="_blank" rel="noopener noreferrer">
{label}
</a>
)
}
return (
<Link href={action.href} className={className}>
{label}
</Link>
)
}
export default function FeatureLandingPage({ page }: { page: ExplorerFeaturePage }) {
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-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-sky-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}
<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 action={action} />
</div>
</div>
</Card>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,419 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import { configApi, type TokenListResponse } from '@/services/api/config'
import {
aggregateLiquidityPools,
featuredLiquiditySymbols,
getLivePlannerProviders,
getRouteBackedPoolAddresses,
getTopLiquidityRoutes,
selectFeaturedLiquidityTokens,
type AggregatedLiquidityPool,
} from '@/services/api/liquidity'
import { plannerApi, type InternalExecutionPlanResponse, type PlannerCapabilitiesResponse } from '@/services/api/planner'
import { routesApi, type MissionControlLiquidityPool, type RouteMatrixResponse } from '@/services/api/routes'
import {
formatCurrency,
formatNumber,
relativeAge,
truncateMiddle,
} from './OperationsPageShell'
const tokenAggregationV1Base = '/token-aggregation/api/v1'
const tokenAggregationV2Base = '/token-aggregation/api/v2'
interface TokenPoolRecord {
symbol: string
pools: MissionControlLiquidityPool[]
}
function routePairLabel(routeId: string, routeLabel: string, tokenIn?: string, tokenOut?: string): string {
return [tokenIn, tokenOut].filter(Boolean).join(' / ') || routeLabel || routeId
}
export default function LiquidityOperationsPage() {
const [tokenList, setTokenList] = useState<TokenListResponse | null>(null)
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
const [plannerCapabilities, setPlannerCapabilities] = useState<PlannerCapabilitiesResponse | null>(null)
const [internalPlan, setInternalPlan] = useState<InternalExecutionPlanResponse | null>(null)
const [tokenPoolRecords, setTokenPoolRecords] = useState<TokenPoolRecord[]>([])
const [loadingError, setLoadingError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
const load = async () => {
const [tokenListResult, routeMatrixResult, plannerCapabilitiesResult, planResult] =
await Promise.allSettled([
configApi.getTokenList(),
routesApi.getRouteMatrix(),
plannerApi.getCapabilities(),
plannerApi.getInternalExecutionPlan(),
])
if (cancelled) return
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
if (routeMatrixResult.status === 'fulfilled') setRouteMatrix(routeMatrixResult.value)
if (plannerCapabilitiesResult.status === 'fulfilled') setPlannerCapabilities(plannerCapabilitiesResult.value)
if (planResult.status === 'fulfilled') setInternalPlan(planResult.value)
if (tokenListResult.status === 'fulfilled') {
const featuredTokens = selectFeaturedLiquidityTokens(tokenListResult.value.tokens || [])
const poolResults = await Promise.allSettled(
featuredTokens.map(async (token) => ({
symbol: token.symbol,
pools: (await routesApi.getTokenPools(token.address)).pools || [],
}))
)
if (!cancelled) {
setTokenPoolRecords(
poolResults
.filter((result): result is PromiseFulfilledResult<TokenPoolRecord> => result.status === 'fulfilled')
.map((result) => result.value)
)
}
}
const failedCount = [
tokenListResult,
routeMatrixResult,
plannerCapabilitiesResult,
planResult,
].filter((result) => result.status === 'rejected').length
if (failedCount === 4) {
setLoadingError('Live liquidity data is temporarily unavailable from the public explorer APIs.')
}
}
load().catch((error) => {
if (!cancelled) {
setLoadingError(
error instanceof Error ? error.message : 'Live liquidity data is temporarily unavailable from the public explorer APIs.'
)
}
})
return () => {
cancelled = true
}
}, [])
const featuredTokens = useMemo(
() => selectFeaturedLiquidityTokens(tokenList?.tokens || []),
[tokenList?.tokens]
)
const aggregatedPools = useMemo(
() => aggregateLiquidityPools(tokenPoolRecords),
[tokenPoolRecords]
)
const livePlannerProviders = useMemo(
() => getLivePlannerProviders(plannerCapabilities),
[plannerCapabilities]
)
const routeBackedPoolAddresses = useMemo(
() => getRouteBackedPoolAddresses(routeMatrix),
[routeMatrix]
)
const highlightedRoutes = useMemo(
() => getTopLiquidityRoutes(routeMatrix, 6),
[routeMatrix]
)
const dexCount = useMemo(
() => new Set(aggregatedPools.map((pool) => pool.dex).filter(Boolean)).size,
[aggregatedPools]
)
const insightLines = useMemo(
() => [
`${formatNumber(routeMatrix?.counts?.liveSwapRoutes)} live swap routes and ${formatNumber(routeMatrix?.counts?.liveBridgeRoutes)} bridge routes are currently published in the public route matrix.`,
`${formatNumber(aggregatedPools.length)} unique pools were discovered across ${formatNumber(featuredTokens.length)} featured Chain 138 liquidity tokens.`,
`${formatNumber(livePlannerProviders.length)} planner providers are live, and the current internal fallback decision is ${internalPlan?.plannerResponse?.decision || 'unknown'}.`,
`${formatNumber(routeBackedPoolAddresses.length)} unique pool addresses are referenced directly by the current live route legs.`,
],
[routeMatrix, aggregatedPools.length, featuredTokens.length, livePlannerProviders.length, internalPlan?.plannerResponse?.decision, routeBackedPoolAddresses.length]
)
const endpointCards = [
{
name: 'Canonical route matrix',
method: 'GET',
href: `${tokenAggregationV1Base}/routes/matrix?includeNonLive=true`,
notes: 'All live and non-live route inventory with counts and pool-backed legs.',
},
{
name: 'Planner capabilities',
method: 'GET',
href: `${tokenAggregationV2Base}/providers/capabilities?chainId=138`,
notes: 'Live provider inventory, published pair coverage, and execution modes.',
},
{
name: 'Internal execution plan',
method: 'POST',
href: `${tokenAggregationV2Base}/routes/internal-execution-plan`,
notes: 'Returns the direct-pool fallback posture that the operator surfaces already verify.',
},
{
name: 'Mission-control token pools',
method: 'GET',
href: `/explorer-api/v1/mission-control/liquidity/token/${featuredTokens[0]?.address || '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22'}/pools`,
notes: 'Cached public pool inventory for a specific Chain 138 token.',
},
]
return (
<main className="container mx-auto px-4 py-6 sm:py-8">
<div className="mb-8 max-w-4xl">
<div className="mb-3 inline-flex rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">
Chain 138 Liquidity Access
</div>
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
Public liquidity, route discovery, and execution access points
</h1>
<p className="text-base leading-7 text-gray-600 dark:text-gray-400 sm:text-lg sm:leading-8">
This page now reads the live explorer APIs instead of hardcoded pool snapshots. It pulls the
public route matrix, planner capabilities, and mission-control token pool inventory together
so integrators can inspect what Chain 138 is actually serving right now.
</p>
</div>
{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-8 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 Pools
</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{formatNumber(aggregatedPools.length)}
</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Dedupe of mission-control pool inventory across featured Chain 138 tokens.
</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">
Route Coverage
</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{formatNumber(routeMatrix?.counts?.filteredLiveRoutes)}
</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
{formatNumber(routeMatrix?.counts?.liveSwapRoutes)} swaps and {formatNumber(routeMatrix?.counts?.liveBridgeRoutes)} bridges.
</div>
</Card>
<Card>
<div className="text-sm text-gray-500 dark:text-gray-400">Planner providers</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{formatNumber(livePlannerProviders.length)}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{formatNumber(dexCount)} DEX families in the current discovered pools.
</div>
</Card>
<Card>
<div className="text-sm text-gray-500 dark:text-gray-400">Fallback posture</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{internalPlan?.plannerResponse?.decision || 'unknown'}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Execution contract {truncateMiddle(internalPlan?.execution?.contractAddress)}
</div>
</Card>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
<Card title="Live Pool Snapshot">
<div className="space-y-4">
{aggregatedPools.slice(0, 8).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="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{(pool.token0?.symbol || '?') + ' / ' + (pool.token1?.symbol || '?')}
</div>
<div className="mt-1 break-all text-xs text-gray-500 dark:text-gray-400">
Pool: {pool.address}
</div>
</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
{pool.dex || 'Unknown DEX'} · TVL {formatCurrency(pool.tvl)}
</div>
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Seen from {pool.sourceSymbols.join(', ')}
</div>
</div>
))}
{aggregatedPools.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">
No live pool inventory is available right now.
</p>
) : null}
</div>
</Card>
<Card title="What Integrators Need To Know">
<div className="space-y-3 text-sm leading-6 text-gray-600 dark:text-gray-400">
{insightLines.map((item) => (
<p key={item}>{item}</p>
))}
<p>
Featured symbols in this view: {featuredLiquiditySymbols.join(', ')}.
</p>
</div>
</Card>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<Card title="Top Route-Backed Liquidity Paths">
<div className="space-y-4">
{highlightedRoutes.map((route) => (
<div
key={route.routeId}
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-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{routePairLabel(route.routeId, route.label || '', route.tokenInSymbol, route.tokenOutSymbol)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formatNumber((route.legs || []).length)} legs · {formatNumber((route.legs || []).filter((leg) => leg.poolAddress).length)} pool-backed
</div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{route.aggregatorFamilies?.join(', ') || 'No provider families listed'}
</div>
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{(route.legs || [])
.map((leg) => leg.poolAddress)
.filter(Boolean)
.map((address) => truncateMiddle(address))
.join(' · ') || 'No pool addresses published'}
</div>
</div>
))}
</div>
</Card>
<Card title="Featured Token Coverage">
<div className="space-y-4">
{featuredTokens.map((token) => {
const matchingRecord = tokenPoolRecords.find((record) => record.symbol === token.symbol)
return (
<div
key={token.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">
{token.symbol}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{token.name || 'Unnamed token'} · {formatNumber(matchingRecord?.pools.length || 0)} mission-control pools
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{truncateMiddle(token.address, 10, 8)}
</div>
</div>
)
})}
</div>
</Card>
</div>
<div className="mb-8">
<Card title="Explorer Access Points">
<div className="grid gap-4 md:grid-cols-2">
{endpointCards.map((endpoint) => (
<a
key={endpoint.href}
href={endpoint.href}
target="_blank"
rel="noopener noreferrer"
className="rounded-2xl border border-gray-200 bg-white p-5 transition hover:border-primary-400 hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
>
<div className="flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="text-base font-semibold text-gray-900 dark:text-white">{endpoint.name}</div>
<span 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">
{endpoint.method}
</span>
</div>
<div className="mt-3 break-all rounded-xl bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-900 dark:text-gray-300">
{endpoint.href}
</div>
<div className="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-400">{endpoint.notes}</div>
</a>
))}
</div>
</Card>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<Card title="Quick Request Examples">
<div className="space-y-4">
{[
`GET ${tokenAggregationV1Base}/routes/matrix?includeNonLive=true`,
`GET ${tokenAggregationV2Base}/providers/capabilities?chainId=138`,
`POST ${tokenAggregationV2Base}/routes/internal-execution-plan`,
`GET /explorer-api/v1/mission-control/liquidity/token/${featuredTokens[0]?.address || '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22'}/pools`,
].map((example) => (
<div key={example} className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<code className="block break-all text-xs leading-6 text-gray-700 dark:text-gray-300">
{example}
</code>
</div>
))}
</div>
</Card>
<Card title="Related Explorer Tools">
<div className="space-y-4 text-sm leading-6 text-gray-600 dark:text-gray-400">
<p>
Use the wallet page for network onboarding, the pools page for a tighter live inventory
view, and this page for the broader route and execution surfaces.
</p>
<p>
The live route matrix was updated {relativeAge(routeMatrix?.updated)}, and the current
route-backed pool set references {formatNumber(routeBackedPoolAddresses.length)} unique
pool addresses.
</p>
<div className="flex flex-wrap gap-3">
<Link
href="/pools"
className="rounded-full bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700"
>
Open pools page
</Link>
<Link
href="/wallet"
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
>
Open wallet tools
</Link>
<a
href="/docs.html"
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
>
Explorer docs
</a>
</div>
</div>
</Card>
</div>
</main>
)
}

View 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>
)
}

View File

@@ -0,0 +1,148 @@
import type { ReactNode } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import type { ExplorerFeatureAction, ExplorerFeaturePage } from '@/data/explorerOperations'
export type StatusTone = 'normal' | 'warning' | 'danger'
function ActionLink({ action }: { action: ExplorerFeatureAction }) {
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
const label = `${action.label} ->`
if (action.external) {
return (
<a href={action.href} className={className} target="_blank" rel="noopener noreferrer">
{label}
</a>
)
}
return (
<Link href={action.href} className={className}>
{label}
</Link>
)
}
export 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`
}
export function formatNumber(value?: number | null): string {
if (typeof value !== 'number' || !Number.isFinite(value)) return '0'
return new Intl.NumberFormat('en-US').format(value)
}
export function formatCurrency(value?: number | null): string {
if (typeof value !== 'number' || !Number.isFinite(value)) return 'Unknown'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}).format(value)
}
export function truncateMiddle(value?: string, start = 8, end = 6): string {
if (!value) return 'Unknown'
if (value.length <= start + end + 3) return value
return `${value.slice(0, start)}...${value.slice(-end)}`
}
export function StatusBadge({
status,
tone = 'normal',
}: {
status: string
tone?: StatusTone
}) {
const toneClass =
tone === 'danger'
? 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-300'
: tone === 'warning'
? 'bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300'
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300'
return (
<span className={`inline-flex rounded-full px-2.5 py-1 text-xs font-semibold uppercase tracking-wide ${toneClass}`}>
{status}
</span>
)
}
export function MetricCard({
title,
value,
description,
className,
}: {
title: string
value: string
description: string
className?: string
}) {
return (
<Card className={className}>
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
{title}
</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{value}</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">{description}</div>
</Card>
)
}
export default function OperationsPageShell({
page,
children,
}: {
page: ExplorerFeaturePage
children: ReactNode
}) {
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-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-sky-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}
{children}
<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 action={action} />
</div>
</div>
</Card>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,193 @@
import { useEffect, useMemo, useState } from 'react'
import { Card } from '@/libs/frontend-ui-primitives'
import { explorerFeaturePages } from '@/data/explorerOperations'
import {
getMissionControlRelays,
getMissionControlRelayLabel,
missionControlApi,
type MissionControlBridgeStatusResponse,
} from '@/services/api/missionControl'
import { plannerApi, type InternalExecutionPlanResponse, type PlannerCapabilitiesResponse } from '@/services/api/planner'
import { routesApi, type RouteMatrixResponse } from '@/services/api/routes'
import OperationsPageShell, {
MetricCard,
StatusBadge,
formatNumber,
relativeAge,
truncateMiddle,
} from './OperationsPageShell'
function relayTone(status?: string): 'normal' | 'warning' | 'danger' {
const normalized = String(status || 'unknown').toLowerCase()
if (['degraded', 'stale', 'stopped', 'down'].includes(normalized)) return 'danger'
if (['paused', 'starting', 'unknown'].includes(normalized)) return 'warning'
return 'normal'
}
export default function OperatorOperationsPage() {
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
const [plannerCapabilities, setPlannerCapabilities] = useState<PlannerCapabilitiesResponse | null>(null)
const [internalPlan, setInternalPlan] = useState<InternalExecutionPlanResponse | null>(null)
const [loadingError, setLoadingError] = useState<string | null>(null)
const page = explorerFeaturePages.operator
useEffect(() => {
let cancelled = false
const load = async () => {
const [bridgeResult, routesResult, capabilitiesResult, planResult] = await Promise.allSettled([
missionControlApi.getBridgeStatus(),
routesApi.getRouteMatrix(),
plannerApi.getCapabilities(),
plannerApi.getInternalExecutionPlan(),
])
if (cancelled) return
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
if (routesResult.status === 'fulfilled') setRouteMatrix(routesResult.value)
if (capabilitiesResult.status === 'fulfilled') setPlannerCapabilities(capabilitiesResult.value)
if (planResult.status === 'fulfilled') setInternalPlan(planResult.value)
const failedCount = [bridgeResult, routesResult, capabilitiesResult, planResult].filter(
(result) => result.status === 'rejected'
).length
if (failedCount === 4) {
setLoadingError('Operator telemetry is temporarily unavailable from the public explorer APIs.')
}
}
load().catch((error) => {
if (!cancelled) {
setLoadingError(error instanceof Error ? error.message : 'Operator telemetry is temporarily unavailable from the public explorer APIs.')
}
})
return () => {
cancelled = true
}
}, [])
const relays = useMemo(() => getMissionControlRelays(bridgeStatus), [bridgeStatus])
const relayEntries = useMemo(() => Object.entries(relays || {}), [relays])
const totalQueue = useMemo(
() =>
relayEntries.reduce((sum, [, relay]) => {
const queueSize = relay.url_probe?.body?.queue?.size ?? relay.file_snapshot?.queue?.size ?? 0
return sum + queueSize
}, 0),
[relayEntries]
)
const providers = plannerCapabilities?.providers || []
const liveProviders = providers.filter((provider) => provider.live)
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="Relay Fleet"
value={bridgeStatus?.data?.status || 'unknown'}
description={`${relayEntries.length} managed lanes · queue ${formatNumber(totalQueue)}`}
className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20"
/>
<MetricCard
title="Live Routes"
value={formatNumber(routeMatrix?.counts?.filteredLiveRoutes)}
description={`${formatNumber(routeMatrix?.counts?.blockedOrPlannedRoutes)} planned or blocked routes remain in the matrix.`}
className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20"
/>
<MetricCard
title="Planner Providers"
value={formatNumber(liveProviders.length)}
description={`${formatNumber(providers.length)} published providers in planner v2 capabilities.`}
/>
<MetricCard
title="Fallback Decision"
value={internalPlan?.plannerResponse?.decision || 'unknown'}
description={
internalPlan?.execution?.contractAddress
? `Execution contract ${truncateMiddle(internalPlan.execution.contractAddress)}`
: 'Latest internal execution plan posture.'
}
/>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<Card title="Managed Relay Lanes">
<div className="space-y-4">
{relayEntries.map(([key, relay]) => {
const snapshot = relay.url_probe?.body || relay.file_snapshot
const status = snapshot?.status || 'unknown'
return (
<div
key={key}
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>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{getMissionControlRelayLabel(key)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{snapshot?.destination?.chain_name || 'Unknown destination'} · queue {formatNumber(snapshot?.queue?.size ?? 0)}
</div>
</div>
<StatusBadge status={status} tone={relayTone(status)} />
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Last source poll {relativeAge(snapshot?.last_source_poll?.at)} · processed {formatNumber(snapshot?.queue?.processed ?? 0)}
</div>
</div>
)
})}
{relayEntries.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">No relay lane data available.</p>
) : null}
</div>
</Card>
<Card title="Execution Readiness">
<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">Internal execution plan</div>
<div className="mt-2 flex items-center gap-3">
<StatusBadge
status={internalPlan?.plannerResponse?.decision || 'unknown'}
tone={internalPlan?.plannerResponse?.decision === 'direct-pool' ? 'normal' : 'warning'}
/>
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Contract {truncateMiddle(internalPlan?.execution?.contractAddress)} · {formatNumber(internalPlan?.plannerResponse?.steps?.length)} planner steps
</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">Live providers</div>
<div className="mt-3 flex flex-wrap gap-2">
{liveProviders.map((provider) => (
<span
key={provider.provider}
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"
>
{provider.provider}
</span>
))}
{liveProviders.length === 0 ? (
<span className="text-sm text-gray-600 dark:text-gray-400">No live providers reported.</span>
) : null}
</div>
</div>
</div>
</Card>
</div>
</OperationsPageShell>
)
}

View File

@@ -0,0 +1,237 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import { configApi, type TokenListResponse } from '@/services/api/config'
import {
aggregateLiquidityPools,
getRouteBackedPoolAddresses,
selectFeaturedLiquidityTokens,
} from '@/services/api/liquidity'
import { routesApi, type MissionControlLiquidityPool, type RouteMatrixResponse } from '@/services/api/routes'
import { formatCurrency, formatNumber, truncateMiddle } from './OperationsPageShell'
interface TokenPoolRecord {
symbol: string
pools: MissionControlLiquidityPool[]
}
export default function PoolsOperationsPage() {
const [tokenList, setTokenList] = useState<TokenListResponse | null>(null)
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
const [tokenPoolRecords, setTokenPoolRecords] = useState<TokenPoolRecord[]>([])
const [loadingError, setLoadingError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
const load = async () => {
const [tokenListResult, routeMatrixResult] = await Promise.allSettled([
configApi.getTokenList(),
routesApi.getRouteMatrix(),
])
if (cancelled) return
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
if (routeMatrixResult.status === 'fulfilled') setRouteMatrix(routeMatrixResult.value)
if (tokenListResult.status === 'fulfilled') {
const featuredTokens = selectFeaturedLiquidityTokens(tokenListResult.value.tokens || [])
const poolResults = await Promise.allSettled(
featuredTokens.map(async (token) => ({
symbol: token.symbol,
pools: (await routesApi.getTokenPools(token.address)).pools || [],
}))
)
if (!cancelled) {
setTokenPoolRecords(
poolResults
.filter((result): result is PromiseFulfilledResult<TokenPoolRecord> => result.status === 'fulfilled')
.map((result) => result.value)
)
}
}
if (tokenListResult.status === 'rejected' && routeMatrixResult.status === 'rejected') {
setLoadingError('Live pool inventory is temporarily unavailable from the public explorer APIs.')
}
}
load().catch((error) => {
if (!cancelled) {
setLoadingError(
error instanceof Error ? error.message : 'Live pool inventory is temporarily unavailable from the public explorer APIs.'
)
}
})
return () => {
cancelled = true
}
}, [])
const featuredTokens = useMemo(
() => selectFeaturedLiquidityTokens(tokenList?.tokens || []),
[tokenList?.tokens]
)
const aggregatedPools = useMemo(
() => aggregateLiquidityPools(tokenPoolRecords),
[tokenPoolRecords]
)
const routeBackedPoolAddresses = useMemo(
() => getRouteBackedPoolAddresses(routeMatrix),
[routeMatrix]
)
const topPools = aggregatedPools.slice(0, 9)
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-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-primary-700">
Live Pool Inventory
</div>
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
Pools
</h1>
<p className="text-base leading-7 text-gray-600 dark:text-gray-400 sm:text-lg sm:leading-8">
This page now summarizes the live pool inventory discovered through mission-control token
pool endpoints and cross-checks it against the current route matrix.
</p>
</div>
{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-8 grid gap-4 md:grid-cols-3">
<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">
Unique pools
</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{formatNumber(aggregatedPools.length)}
</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Discovered across {formatNumber(featuredTokens.length)} featured Chain 138 tokens.
</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">
Route-backed pools
</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{formatNumber(routeBackedPoolAddresses.length)}
</div>
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
Unique pool addresses referenced by the live route matrix.
</div>
</Card>
<Card>
<div className="text-sm text-gray-500 dark:text-gray-400">Featured coverage</div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{formatNumber(tokenPoolRecords.filter((record) => record.pools.length > 0).length)}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Featured tokens currently returning at least one live pool.
</div>
</Card>
</div>
<div className="mb-8">
<Card title="Live Pool Cards">
<div className="grid gap-4 lg:grid-cols-3">
{topPools.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 {formatCurrency(pool.tvl)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{truncateMiddle(pool.address, 10, 8)}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Seen from {pool.sourceSymbols.join(', ')}
</div>
</div>
))}
{topPools.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">No live pools available right now.</p>
) : null}
</div>
</Card>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<Card title="Featured Token Pool Counts">
<div className="space-y-4">
{featuredTokens.map((token) => {
const record = tokenPoolRecords.find((entry) => entry.symbol === token.symbol)
return (
<div
key={token.address}
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
>
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{token.symbol}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{token.name || 'Unnamed token'}
</div>
</div>
<div className="text-sm font-semibold text-gray-900 dark:text-white">
{formatNumber(record?.pools.length || 0)} pools
</div>
</div>
</div>
)
})}
</div>
</Card>
<Card title="Liquidity Shortcuts">
<div className="space-y-4 text-sm leading-6 text-gray-600 dark:text-gray-400">
<p>
The broader liquidity page now shows live route, planner, and pool access together.
</p>
<p>
The current route matrix publishes {formatNumber(routeMatrix?.counts?.liveSwapRoutes)} live
swap routes and {formatNumber(routeMatrix?.counts?.liveBridgeRoutes)} bridge routes.
</p>
<div className="flex flex-wrap gap-3">
<Link
href="/liquidity"
className="rounded-full bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700"
>
Open liquidity access
</Link>
<Link
href="/routes"
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
>
Open routes page
</Link>
<Link
href="/wallet"
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
>
Open wallet tools
</Link>
</div>
</div>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,401 @@
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'
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() {
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
const [networks, setNetworks] = useState<ExplorerNetwork[]>([])
const [pools, setPools] = useState<MissionControlLiquidityPool[]>([])
const [loadingError, setLoadingError] = useState<string | null>(null)
const page = explorerFeaturePages.routes
useEffect(() => {
let cancelled = false
const load = async () => {
const [matrixResult, networksResult, poolsResult] = await Promise.allSettled([
routesApi.getRouteMatrix(),
routesApi.getNetworks(),
routesApi.getTokenPools(canonicalLiquidityToken),
])
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 (
matrixResult.status === 'rejected' &&
networksResult.status === 'rejected' &&
poolsResult.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])
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 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={'external' in action ? action.external : undefined}
/>
</div>
</div>
</Card>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,189 @@
import { useEffect, useMemo, useState } from 'react'
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'
import { statsApi, type ExplorerStats } from '@/services/api/stats'
import OperationsPageShell, {
MetricCard,
StatusBadge,
formatNumber,
relativeAge,
} from './OperationsPageShell'
export default function SystemOperationsPage() {
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
const [networksConfig, setNetworksConfig] = useState<NetworksConfigResponse | null>(null)
const [tokenList, setTokenList] = useState<TokenListResponse | null>(null)
const [capabilities, setCapabilities] = useState<CapabilitiesResponse | null>(null)
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
const [stats, setStats] = useState<ExplorerStats | null>(null)
const [loadingError, setLoadingError] = useState<string | null>(null)
const page = explorerFeaturePages.system
useEffect(() => {
let cancelled = false
const load = async () => {
const [bridgeResult, networksResult, tokenListResult, capabilitiesResult, routesResult, statsResult] =
await Promise.allSettled([
missionControlApi.getBridgeStatus(),
configApi.getNetworks(),
configApi.getTokenList(),
configApi.getCapabilities(),
routesApi.getRouteMatrix(),
statsApi.get(),
])
if (cancelled) return
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
if (networksResult.status === 'fulfilled') setNetworksConfig(networksResult.value)
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
if (capabilitiesResult.status === 'fulfilled') setCapabilities(capabilitiesResult.value)
if (routesResult.status === 'fulfilled') setRouteMatrix(routesResult.value)
if (statsResult.status === 'fulfilled') setStats(statsResult.value)
const failedCount = [
bridgeResult,
networksResult,
tokenListResult,
capabilitiesResult,
routesResult,
statsResult,
].filter((result) => result.status === 'rejected').length
if (failedCount === 6) {
setLoadingError('System inventory data is temporarily unavailable from the public explorer APIs.')
}
}
load().catch((error) => {
if (!cancelled) {
setLoadingError(error instanceof Error ? error.message : 'System inventory data is temporarily unavailable from the public explorer APIs.')
}
})
return () => {
cancelled = true
}
}, [])
const relays = useMemo(() => getMissionControlRelays(bridgeStatus), [bridgeStatus])
const chainStatus = bridgeStatus?.data?.chains?.['138']
const chainCoverage = useMemo(
() => new Set((tokenList?.tokens || []).map((token) => token.chainId).filter(Boolean)).size,
[tokenList]
)
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="Published Networks"
value={formatNumber(networksConfig?.chains?.length)}
description={`Default chain ${networksConfig?.defaultChainId ?? 'unknown'} in wallet onboarding.`}
className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20"
/>
<MetricCard
title="Relay Lanes"
value={formatNumber(relays ? Object.keys(relays).length : 0)}
description={`${bridgeStatus?.data?.status || 'unknown'} public bridge posture across managed lanes.`}
className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20"
/>
<MetricCard
title="Token Coverage"
value={formatNumber((tokenList?.tokens || []).length)}
description={`${formatNumber(chainCoverage)} chain catalogs served through the public token list.`}
/>
<MetricCard
title="RPC Methods"
value={formatNumber(capabilities?.http?.supportedMethods?.length)}
description={`${formatNumber(capabilities?.tracing?.supportedMethods?.length)} tracing methods published.`}
/>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<Card title="Topology 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">Chain 138 RPC</div>
<div className="mt-2 flex items-center gap-3">
<StatusBadge status={chainStatus?.status || 'unknown'} tone={chainStatus?.status === 'operational' ? 'normal' : 'warning'} />
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Head age {chainStatus?.head_age_sec != null ? `${Math.round(chainStatus.head_age_sec)}s` : 'Unknown'} · latency {chainStatus?.latency_ms != null ? `${Math.round(chainStatus.latency_ms)}ms` : '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">Route matrix</div>
<div className="mt-2 text-lg font-semibold text-gray-900 dark:text-white">
{formatNumber(routeMatrix?.counts?.filteredLiveRoutes)} live routes
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Updated {relativeAge(routeMatrix?.updated)} · {formatNumber(routeMatrix?.counts?.blockedOrPlannedRoutes)} planned or blocked
</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">Explorer index</div>
<div className="mt-2 text-lg font-semibold text-gray-900 dark:text-white">
{formatNumber(stats?.total_blocks)} blocks
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
{formatNumber(stats?.total_transactions)} transactions · {formatNumber(stats?.total_addresses)} addresses
</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 compatibility</div>
<div className="mt-2 flex items-center gap-3">
<StatusBadge
status={
capabilities?.walletSupport?.walletAddEthereumChain && capabilities?.walletSupport?.walletWatchAsset
? 'ready'
: 'partial'
}
tone={
capabilities?.walletSupport?.walletAddEthereumChain && capabilities?.walletSupport?.walletWatchAsset
? 'normal'
: 'warning'
}
/>
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Public capabilities JSON is wired for chain-add and token-add flows.
</div>
</div>
</div>
</Card>
<Card title="Network Inventory">
<div className="grid gap-3 sm:grid-cols-2">
{(networksConfig?.chains || []).map((chain) => (
<div
key={`${chain.chainIdDecimal}-${chain.shortName}`}
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">
{chain.chainName || chain.shortName || `Chain ${chain.chainIdDecimal}`}
</div>
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Chain ID {chain.chainIdDecimal ?? 'Unknown'} · short name {chain.shortName || 'Unknown'}
</div>
</div>
))}
{(networksConfig?.chains || []).length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">No network inventory available.</p>
) : null}
</div>
</Card>
</div>
</OperationsPageShell>
)
}

View File

@@ -0,0 +1,188 @@
import { useEffect, useMemo, useState } from 'react'
import { Card } from '@/libs/frontend-ui-primitives'
import { explorerFeaturePages } from '@/data/explorerOperations'
import {
getMissionControlRelays,
missionControlApi,
type MissionControlBridgeStatusResponse,
type MissionControlRelayPayload,
} from '@/services/api/missionControl'
import { plannerApi, type InternalExecutionPlanResponse, type PlannerCapabilitiesResponse } from '@/services/api/planner'
import OperationsPageShell, {
MetricCard,
StatusBadge,
formatNumber,
relativeAge,
truncateMiddle,
} from './OperationsPageShell'
function relayTone(status?: string): 'normal' | 'warning' | 'danger' {
const normalized = String(status || 'unknown').toLowerCase()
if (['degraded', 'stale', 'stopped', 'down'].includes(normalized)) return 'danger'
if (['paused', 'starting', 'unknown'].includes(normalized)) return 'warning'
return 'normal'
}
function relaySnapshot(relay: MissionControlRelayPayload | undefined) {
return relay?.url_probe?.body || relay?.file_snapshot
}
export default function WethOperationsPage() {
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
const [plannerCapabilities, setPlannerCapabilities] = useState<PlannerCapabilitiesResponse | null>(null)
const [internalPlan, setInternalPlan] = useState<InternalExecutionPlanResponse | null>(null)
const [loadingError, setLoadingError] = useState<string | null>(null)
const page = explorerFeaturePages.weth
useEffect(() => {
let cancelled = false
const load = async () => {
const [bridgeResult, capabilitiesResult, planResult] = await Promise.allSettled([
missionControlApi.getBridgeStatus(),
plannerApi.getCapabilities(),
plannerApi.getInternalExecutionPlan(),
])
if (cancelled) return
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
if (capabilitiesResult.status === 'fulfilled') setPlannerCapabilities(capabilitiesResult.value)
if (planResult.status === 'fulfilled') setInternalPlan(planResult.value)
const failedCount = [bridgeResult, capabilitiesResult, planResult].filter(
(result) => result.status === 'rejected'
).length
if (failedCount === 3) {
setLoadingError('WETH operations data is temporarily unavailable from the public explorer APIs.')
}
}
load().catch((error) => {
if (!cancelled) {
setLoadingError(error instanceof Error ? error.message : 'WETH operations data is temporarily unavailable from the public explorer APIs.')
}
})
return () => {
cancelled = true
}
}, [])
const relays = useMemo(() => getMissionControlRelays(bridgeStatus), [bridgeStatus])
const mainnetWeth = relaySnapshot(relays?.mainnet_weth)
const mainnetCw = relaySnapshot(relays?.mainnet_cw)
const wethProviders = useMemo(
() =>
(plannerCapabilities?.providers || []).filter((provider) =>
(provider.pairs || []).some(
(pair) => pair.tokenInSymbol === 'WETH' || pair.tokenOutSymbol === 'WETH'
)
),
[plannerCapabilities]
)
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="Mainnet WETH Lane"
value={mainnetWeth?.status || 'unknown'}
description={`Queue ${formatNumber(mainnetWeth?.queue?.size ?? 0)} · ${mainnetWeth?.destination?.chain_name || 'destination unknown'}`}
className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20"
/>
<MetricCard
title="Mainnet cW Lane"
value={mainnetCw?.status || 'unknown'}
description={`Queue ${formatNumber(mainnetCw?.queue?.size ?? 0)} · ${mainnetCw?.destination?.chain_name || 'destination unknown'}`}
className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20"
/>
<MetricCard
title="WETH Providers"
value={formatNumber(wethProviders.length)}
description="Providers that currently publish at least one WETH leg in planner v2."
/>
<MetricCard
title="Fallback Decision"
value={internalPlan?.plannerResponse?.decision || 'unknown'}
description={
internalPlan?.execution?.contractAddress
? `Execution contract ${truncateMiddle(internalPlan.execution.contractAddress)}`
: 'Current internal execution-plan posture for a WETH route.'
}
/>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<Card title="Mainnet Bridge Lanes">
<div className="space-y-4">
{[
{ label: 'Mainnet WETH', snapshot: mainnetWeth },
{ label: 'Mainnet cW', snapshot: mainnetCw },
].map(({ label, snapshot }) => (
<div
key={label}
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>
<div className="text-base font-semibold text-gray-900 dark:text-white">{label}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Source {snapshot?.source?.chain_name || 'Unknown'} · destination {snapshot?.destination?.chain_name || 'Unknown'}
</div>
</div>
<StatusBadge status={snapshot?.status || 'unknown'} tone={relayTone(snapshot?.status)} />
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
Queue {formatNumber(snapshot?.queue?.size ?? 0)} · last poll {relativeAge(snapshot?.last_source_poll?.at)}
</div>
</div>
))}
</div>
</Card>
<Card title="WETH Route-Ready Providers">
<div className="space-y-4">
{wethProviders.map((provider) => {
const samplePairs = (provider.pairs || [])
.filter((pair) => pair.tokenInSymbol === 'WETH' || pair.tokenOutSymbol === 'WETH')
.slice(0, 3)
return (
<div
key={provider.provider}
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>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{provider.provider}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{provider.executionMode || 'unknown mode'} · {(provider.supportedLegTypes || []).join(', ') || 'no leg types'}
</div>
</div>
<StatusBadge status={provider.live ? 'live' : 'inactive'} tone={provider.live ? 'normal' : 'warning'} />
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
{samplePairs.map((pair) => `${pair.tokenInSymbol} -> ${pair.tokenOutSymbol}`).join(' · ') || 'No WETH pairs published'}
</div>
</div>
)
})}
{wethProviders.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">No WETH-aware providers reported.</p>
) : null}
</div>
</Card>
</div>
</OperationsPageShell>
)
}

View File

@@ -55,9 +55,16 @@ type TokenListCatalog = {
type CapabilitiesCatalog = {
name?: string
version?: {
major?: number
minor?: number
patch?: number
}
timestamp?: string
chainId?: number
chainName?: string
rpcUrl?: string
explorerUrl?: string
explorerApiUrl?: string
generatedBy?: string
walletSupport?: {
@@ -128,6 +135,80 @@ const FEATURED_TOKEN_SYMBOLS = ['cUSDT', 'cUSDC', 'USDT', 'USDC', 'cXAUC', 'cXAU
/** npm-published Snap using open Snap permissions only; stable MetaMask still requires MetaMasks install allowlist. */
const CHAIN138_OPEN_SNAP_ID = 'npm:chain138-open-snap' as const
const FALLBACK_CAPABILITIES_138: CapabilitiesCatalog = {
name: 'Chain 138 RPC Capabilities',
version: { major: 1, minor: 1, patch: 0 },
timestamp: '2026-03-28T00:00:00Z',
generatedBy: 'SolaceScanScout',
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet',
rpcUrl: 'https://rpc-http-pub.d-bis.org',
explorerUrl: 'https://explorer.d-bis.org',
explorerApiUrl: 'https://explorer.d-bis.org/api/v2',
walletSupport: {
walletAddEthereumChain: true,
walletWatchAsset: true,
notes: [
'MetaMask primarily relies on JSON-RPC correctness for balances, gas estimation, calls, and transaction submission.',
'Explorer-served network metadata and token list metadata complement wallet UX but do not replace RPC method support.',
],
},
http: {
supportedMethods: [
'web3_clientVersion',
'net_version',
'eth_chainId',
'eth_blockNumber',
'eth_syncing',
'eth_gasPrice',
'eth_maxPriorityFeePerGas',
'eth_feeHistory',
'eth_estimateGas',
'eth_getCode',
],
unsupportedMethods: [],
notes: [
'eth_feeHistory is available for wallet fee estimation.',
'eth_maxPriorityFeePerGas is exposed on the public RPC for wallet-grade fee suggestion compatibility.',
],
},
tracing: {
supportedMethods: ['trace_block', 'trace_replayBlockTransactions'],
unsupportedMethods: ['debug_traceBlockByNumber'],
notes: [
'TRACE support is enabled for explorer-grade indexing and internal transaction analysis.',
'Debug tracing is intentionally not enabled on the public RPC tier.',
],
},
}
function isTokenListToken(value: unknown): value is TokenListToken {
if (!value || typeof value !== 'object') return false
const candidate = value as Partial<TokenListToken>
return (
typeof candidate.chainId === 'number' &&
typeof candidate.address === 'string' &&
candidate.address.trim().length > 0 &&
typeof candidate.name === 'string' &&
typeof candidate.symbol === 'string' &&
typeof candidate.decimals === 'number'
)
}
function isCapabilitiesCatalog(value: unknown): value is CapabilitiesCatalog {
if (!value || typeof value !== 'object') return false
const candidate = value as Partial<CapabilitiesCatalog>
return (
typeof candidate.chainId === 'number' &&
typeof candidate.chainName === 'string' &&
candidate.chainName.trim().length > 0 &&
typeof candidate.rpcUrl === 'string' &&
candidate.rpcUrl.trim().length > 0
)
}
function getApiBase() {
return resolveExplorerApiBase({
serverFallback: 'https://explorer.d-bis.org',
@@ -152,6 +233,10 @@ export function AddToMetaMask() {
const tokenListUrl = `${apiBase}/api/config/token-list`
const networksUrl = `${apiBase}/api/config/networks`
const capabilitiesUrl = `${apiBase}/api/config/capabilities`
const staticCapabilitiesUrl =
typeof window !== 'undefined'
? `${window.location.origin.replace(/\/$/, '')}/config/CHAIN138_RPC_CAPABILITIES.json`
: `${apiBase}/config/CHAIN138_RPC_CAPABILITIES.json`
useEffect(() => {
let active = true
@@ -180,21 +265,46 @@ export function AddToMetaMask() {
fetchJson(capabilitiesUrl),
])
let resolvedCapabilities = capabilitiesResponse
if (!isCapabilitiesCatalog(resolvedCapabilities.json)) {
const staticCapabilitiesResponse = await fetchJson(staticCapabilitiesUrl)
if (isCapabilitiesCatalog(staticCapabilitiesResponse.json)) {
resolvedCapabilities = {
json: staticCapabilitiesResponse.json,
meta: {
source: staticCapabilitiesResponse.meta.source || 'public-static-fallback',
lastModified: staticCapabilitiesResponse.meta.lastModified,
},
}
} else {
resolvedCapabilities = {
json: FALLBACK_CAPABILITIES_138,
meta: {
source: 'frontend-fallback',
lastModified: FALLBACK_CAPABILITIES_138.timestamp || null,
},
}
}
}
if (!active) return
setNetworks(networksResponse.json)
setTokenList(tokenListResponse.json)
setCapabilities(capabilitiesResponse.json)
setCapabilities(resolvedCapabilities.json)
setNetworksMeta(networksResponse.meta)
setTokenListMeta(tokenListResponse.meta)
setCapabilitiesMeta(capabilitiesResponse.meta)
setCapabilitiesMeta(resolvedCapabilities.meta)
} catch {
if (!active) return
setNetworks(null)
setTokenList(null)
setCapabilities(null)
setCapabilities(FALLBACK_CAPABILITIES_138)
setNetworksMeta(null)
setTokenListMeta(null)
setCapabilitiesMeta(null)
setCapabilitiesMeta({
source: 'frontend-fallback',
lastModified: FALLBACK_CAPABILITIES_138.timestamp || null,
})
} finally {
if (active) {
timer = setTimeout(() => {
@@ -210,7 +320,12 @@ export function AddToMetaMask() {
active = false
if (timer) clearTimeout(timer)
}
}, [capabilitiesUrl, networksUrl, tokenListUrl])
}, [capabilitiesUrl, networksUrl, staticCapabilitiesUrl, tokenListUrl])
const catalogTokens = useMemo(
() => (Array.isArray(tokenList?.tokens) ? tokenList.tokens.filter(isTokenListToken) : []),
[tokenList],
)
const chains = useMemo(() => {
const chainMap = new Map<number, WalletChain>()
@@ -230,7 +345,7 @@ export function AddToMetaMask() {
const featuredTokens = useMemo(() => {
const tokenMap = new Map<string, TokenListToken>()
for (const token of tokenList?.tokens || []) {
for (const token of catalogTokens) {
if (token.chainId !== 138) continue
if (!FEATURED_TOKEN_SYMBOLS.includes(token.symbol)) continue
tokenMap.set(token.symbol, token)
@@ -239,7 +354,7 @@ export function AddToMetaMask() {
return FEATURED_TOKEN_SYMBOLS
.map((symbol) => tokenMap.get(symbol))
.filter((token): token is TokenListToken => !!token)
}, [tokenList])
}, [catalogTokens])
const addChain = async (chain: WalletChain) => {
setError(null)
@@ -304,6 +419,11 @@ export function AddToMetaMask() {
setError(null)
setStatus(null)
if (!isTokenListToken(token)) {
setError('Token metadata is incomplete right now. Refresh the page and try again.')
return
}
if (!ethereum) {
setError('MetaMask or another Web3 wallet is not installed.')
return
@@ -312,7 +432,7 @@ export function AddToMetaMask() {
try {
const added = await ethereum.request({
method: 'wallet_watchAsset',
params: [{
params: {
type: 'ERC20',
options: {
address: token.address,
@@ -320,7 +440,7 @@ export function AddToMetaMask() {
decimals: token.decimals,
image: token.logoURI,
},
}],
},
})
setStatus(added ? `Added ${token.symbol} to your wallet.` : `${token.symbol} request was dismissed.`)
@@ -342,11 +462,15 @@ export function AddToMetaMask() {
}
}
const tokenCount138 = (tokenList?.tokens || []).filter((token) => token.chainId === 138).length
const tokenCount138 = catalogTokens.filter((token) => token.chainId === 138).length
const metadataKeywordString = (tokenList?.keywords || []).join(', ')
const supportedHTTPMethods = capabilities?.http?.supportedMethods || []
const unsupportedHTTPMethods = capabilities?.http?.unsupportedMethods || []
const supportedTraceMethods = capabilities?.tracing?.supportedMethods || []
const displayedCapabilitiesUrl =
capabilitiesMeta?.source === 'public-static-fallback' || capabilitiesMeta?.source === 'frontend-fallback'
? staticCapabilitiesUrl
: capabilitiesUrl
return (
<div className="space-y-4 rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800 sm:p-5">
@@ -432,12 +556,12 @@ export function AddToMetaMask() {
</div>
<div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Capabilities URL</p>
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{capabilitiesUrl}</code>
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{displayedCapabilitiesUrl}</code>
<div className="mt-2 flex flex-wrap gap-2">
<button type="button" onClick={() => copyText(capabilitiesUrl, 'capabilities URL')} className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
<button type="button" onClick={() => copyText(displayedCapabilitiesUrl, 'capabilities URL')} className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
Copy URL
</button>
<a href={capabilitiesUrl} target="_blank" rel="noopener noreferrer" className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
<a href={displayedCapabilitiesUrl} target="_blank" rel="noopener noreferrer" className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
Open JSON
</a>
</div>