253 lines
6.8 KiB
TypeScript
253 lines
6.8 KiB
TypeScript
|
|
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
|
||
|
|
)
|
||
|
|
},
|
||
|
|
}
|