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:
252
frontend/src/services/api/missionControl.ts
Normal file
252
frontend/src/services/api/missionControl.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { getExplorerApiBase } from './blockscout'
|
||||
|
||||
export interface MissionControlRelaySummary {
|
||||
text: string
|
||||
tone: 'normal' | 'warning' | 'danger'
|
||||
items: MissionControlRelayItemSummary[]
|
||||
}
|
||||
|
||||
export interface MissionControlRelayItemSummary {
|
||||
key: string
|
||||
label: string
|
||||
status: string
|
||||
text: string
|
||||
tone: 'normal' | 'warning' | 'danger'
|
||||
}
|
||||
|
||||
export interface MissionControlRelaySnapshot {
|
||||
status?: string
|
||||
service?: {
|
||||
profile?: string
|
||||
}
|
||||
source?: {
|
||||
chain_name?: string
|
||||
bridge_filter?: string
|
||||
}
|
||||
destination?: {
|
||||
chain_name?: string
|
||||
relay_bridge?: string
|
||||
relay_bridge_default?: string
|
||||
}
|
||||
queue?: {
|
||||
size?: number
|
||||
processed?: number
|
||||
failed?: number
|
||||
}
|
||||
last_source_poll?: {
|
||||
at?: string
|
||||
ok?: boolean
|
||||
logs_fetched?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MissionControlRelayProbe {
|
||||
ok?: boolean
|
||||
body?: MissionControlRelaySnapshot
|
||||
}
|
||||
|
||||
export interface MissionControlRelayPayload {
|
||||
url_probe?: MissionControlRelayProbe
|
||||
file_snapshot?: MissionControlRelaySnapshot
|
||||
file_snapshot_error?: string
|
||||
}
|
||||
|
||||
export interface MissionControlChainStatus {
|
||||
status?: string
|
||||
name?: string
|
||||
head_age_sec?: number
|
||||
latency_ms?: number
|
||||
block_number?: string
|
||||
}
|
||||
|
||||
export interface MissionControlBridgeStatusResponse {
|
||||
data?: {
|
||||
status?: string
|
||||
checked_at?: string
|
||||
chains?: Record<string, MissionControlChainStatus>
|
||||
ccip_relay?: MissionControlRelayPayload
|
||||
ccip_relays?: Record<string, MissionControlRelayPayload>
|
||||
}
|
||||
}
|
||||
|
||||
const missionControlRelayLabels: Record<string, string> = {
|
||||
mainnet: 'Mainnet',
|
||||
mainnet_weth: 'Mainnet WETH',
|
||||
mainnet_cw: 'Mainnet cW',
|
||||
bsc: 'BSC',
|
||||
avax: 'Avalanche',
|
||||
avalanche: 'Avalanche',
|
||||
avax_cw: 'Avalanche cW',
|
||||
avax_to_138: 'Avalanche -> 138',
|
||||
}
|
||||
|
||||
function getMissionControlStreamUrl(): string {
|
||||
return `${getExplorerApiBase()}/explorer-api/v1/mission-control/stream`
|
||||
}
|
||||
|
||||
function getMissionControlBridgeStatusUrl(): string {
|
||||
return `${getExplorerApiBase()}/explorer-api/v1/track1/bridge/status`
|
||||
}
|
||||
|
||||
function relativeAge(isoString?: string): string {
|
||||
if (!isoString) return ''
|
||||
const parsed = Date.parse(isoString)
|
||||
if (!Number.isFinite(parsed)) return ''
|
||||
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 summarizeMissionControlRelay(
|
||||
response: MissionControlBridgeStatusResponse | null | undefined
|
||||
): MissionControlRelaySummary | null {
|
||||
const relays = getMissionControlRelays(response)
|
||||
if (!relays) return null
|
||||
|
||||
const items = Object.entries(relays)
|
||||
.map(([key, relay]): MissionControlRelayItemSummary | null => {
|
||||
const label = getMissionControlRelayLabel(key)
|
||||
|
||||
if (relay.url_probe && relay.url_probe.ok === false) {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
status: 'down',
|
||||
text: `${label}: probe failed`,
|
||||
tone: 'danger',
|
||||
}
|
||||
}
|
||||
|
||||
if (relay.file_snapshot_error) {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
status: 'snapshot-error',
|
||||
text: `${label}: snapshot error`,
|
||||
tone: 'danger',
|
||||
}
|
||||
}
|
||||
|
||||
const snapshot = relay.url_probe?.body || relay.file_snapshot
|
||||
if (!snapshot) {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
status: 'configured',
|
||||
text: `${label}: probe configured`,
|
||||
tone: 'normal',
|
||||
}
|
||||
}
|
||||
|
||||
const status = String(snapshot.status || 'unknown').toLowerCase()
|
||||
const destination = snapshot.destination?.chain_name
|
||||
const queueSize = snapshot.queue?.size
|
||||
const pollAge = relativeAge(snapshot.last_source_poll?.at)
|
||||
|
||||
let text = `${label}: ${status}`
|
||||
if (destination) text += ` -> ${destination}`
|
||||
if (queueSize != null) text += ` · queue ${queueSize}`
|
||||
if (pollAge) text += ` · polled ${pollAge}`
|
||||
|
||||
let tone: MissionControlRelaySummary['tone'] = 'normal'
|
||||
if (['paused', 'starting'].includes(status)) {
|
||||
tone = 'warning'
|
||||
}
|
||||
if (['degraded', 'stale', 'stopped', 'down'].includes(status)) {
|
||||
tone = 'danger'
|
||||
}
|
||||
|
||||
return { key, label, status, text, tone }
|
||||
})
|
||||
.filter((item): item is MissionControlRelayItemSummary => item !== null)
|
||||
|
||||
if (items.length === 0) return null
|
||||
|
||||
const tone: MissionControlRelaySummary['tone'] = items.some((item) => item.tone === 'danger')
|
||||
? 'danger'
|
||||
: items.some((item) => item.tone === 'warning')
|
||||
? 'warning'
|
||||
: 'normal'
|
||||
|
||||
const text =
|
||||
items.length === 1
|
||||
? items[0].text
|
||||
: tone === 'normal'
|
||||
? `${items.length} relay lanes operational`
|
||||
: `Relay lanes need attention`
|
||||
|
||||
return { text, tone, items }
|
||||
}
|
||||
|
||||
export function getMissionControlRelays(
|
||||
response: MissionControlBridgeStatusResponse | null | undefined
|
||||
): Record<string, MissionControlRelayPayload> | null {
|
||||
return response?.data?.ccip_relays && Object.keys(response.data.ccip_relays).length > 0
|
||||
? response.data.ccip_relays
|
||||
: response?.data?.ccip_relay
|
||||
? { mainnet: response.data.ccip_relay }
|
||||
: null
|
||||
}
|
||||
|
||||
export function getMissionControlRelayLabel(key: string): string {
|
||||
return missionControlRelayLabels[key] || key.toUpperCase()
|
||||
}
|
||||
|
||||
export const missionControlApi = {
|
||||
getBridgeStatus: async (): Promise<MissionControlBridgeStatusResponse> => {
|
||||
const response = await fetch(getMissionControlBridgeStatusUrl())
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
return (await response.json()) as MissionControlBridgeStatusResponse
|
||||
},
|
||||
|
||||
getRelaySummary: async (): Promise<MissionControlRelaySummary | null> => {
|
||||
const json = await missionControlApi.getBridgeStatus()
|
||||
return summarizeMissionControlRelay(json)
|
||||
},
|
||||
|
||||
subscribeBridgeStatus: (
|
||||
onStatus: (status: MissionControlBridgeStatusResponse) => void,
|
||||
onError?: (error: unknown) => void
|
||||
): (() => void) => {
|
||||
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') {
|
||||
onError?.(new Error('EventSource is not available in this environment'))
|
||||
return () => {}
|
||||
}
|
||||
|
||||
const eventSource = new window.EventSource(getMissionControlStreamUrl())
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data) as MissionControlBridgeStatusResponse
|
||||
onStatus(payload)
|
||||
} catch (error) {
|
||||
onError?.(error)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource.onerror = () => {
|
||||
onError?.(new Error('Mission-control live stream connection lost'))
|
||||
}
|
||||
|
||||
return () => {
|
||||
eventSource.close()
|
||||
}
|
||||
},
|
||||
|
||||
subscribeRelaySummary: (
|
||||
onSummary: (summary: MissionControlRelaySummary | null) => void,
|
||||
onError?: (error: unknown) => void
|
||||
): (() => void) => {
|
||||
return missionControlApi.subscribeBridgeStatus(
|
||||
(payload) => {
|
||||
onSummary(summarizeMissionControlRelay(payload))
|
||||
},
|
||||
onError
|
||||
)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user