Add bridge lane health API and config-ready lane UI for Tier A Week 3.
Probe LINK balances on CCIP bridge contracts, expose proof-transfer metadata on bridge status, and render funded/unfunded lane health on /bridge with extended smoke coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
255
backend/api/track1/bridge_lanes.go
Normal file
255
backend/api/track1/bridge_lanes.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed bridge_lanes_default.json
|
||||
var defaultBridgeLanesJSON []byte
|
||||
|
||||
type bridgeLaneDefinition struct {
|
||||
Key string `json:"key"`
|
||||
ChainName string `json:"chain_name"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
ConfigReady bool `json:"config_ready"`
|
||||
RPCEnvs []string `json:"rpc_envs"`
|
||||
RPCDefault string `json:"rpc_default"`
|
||||
LinkToken string `json:"link_token"`
|
||||
WETH9Bridge string `json:"weth9_bridge"`
|
||||
WETH10Bridge string `json:"weth10_bridge"`
|
||||
}
|
||||
|
||||
type bridgeLanesConfig struct {
|
||||
Updated string `json:"updated"`
|
||||
MinLinkWei string `json:"min_link_wei"`
|
||||
Lanes []bridgeLaneDefinition `json:"lanes"`
|
||||
}
|
||||
|
||||
func loadBridgeLanesConfig() bridgeLanesConfig {
|
||||
path := strings.TrimSpace(os.Getenv("MISSION_CONTROL_BRIDGE_LANES_JSON"))
|
||||
if path != "" {
|
||||
if b, err := os.ReadFile(path); err == nil && len(b) > 0 {
|
||||
var cfg bridgeLanesConfig
|
||||
if json.Unmarshal(b, &cfg) == nil && len(cfg.Lanes) > 0 {
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
var cfg bridgeLanesConfig
|
||||
_ = json.Unmarshal(defaultBridgeLanesJSON, &cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func resolveLaneRPC(def bridgeLaneDefinition) string {
|
||||
for _, key := range def.RPCEnvs {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
for _, row := range ParseExtraRPCProbes() {
|
||||
chainKey := row[2]
|
||||
if chainKey == strconv.FormatInt(def.ChainID, 10) {
|
||||
return row[1]
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(def.RPCDefault)
|
||||
}
|
||||
|
||||
func erc20BalanceOf(ctx context.Context, rpcURL, tokenAddress, holderAddress string) (string, error) {
|
||||
tokenAddress = strings.ToLower(strings.TrimSpace(tokenAddress))
|
||||
holderAddress = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(holderAddress), "0x"))
|
||||
if len(holderAddress) != 40 {
|
||||
return "0", nil
|
||||
}
|
||||
data := "0x70a08231" + strings.Repeat("0", 24) + holderAddress
|
||||
raw, _, err := postJSONRPC(ctx, bridgeLaneHTTPClient(), rpcURL, "eth_call", []interface{}{
|
||||
map[string]interface{}{
|
||||
"to": tokenAddress,
|
||||
"data": data,
|
||||
},
|
||||
"latest",
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var hex string
|
||||
if err := json.Unmarshal(raw, &hex); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hex = strings.TrimSpace(hex)
|
||||
if hex == "" || hex == "0x" {
|
||||
return "0", nil
|
||||
}
|
||||
value := new(big.Int)
|
||||
if _, ok := value.SetString(strings.TrimPrefix(hex, "0x"), 16); !ok {
|
||||
return "0", nil
|
||||
}
|
||||
return value.String(), nil
|
||||
}
|
||||
|
||||
func bridgeLaneHTTPClient() *http.Client {
|
||||
return &http.Client{Timeout: 6 * time.Second}
|
||||
}
|
||||
|
||||
func bridgeFundingStatus(linkBalanceWei, minLinkWei string) string {
|
||||
balance, okBalance := new(big.Int).SetString(strings.TrimSpace(linkBalanceWei), 10)
|
||||
minimum, okMin := new(big.Int).SetString(strings.TrimSpace(minLinkWei), 10)
|
||||
if !okBalance || !okMin {
|
||||
return "unknown"
|
||||
}
|
||||
if balance.Cmp(minimum) >= 0 {
|
||||
return "funded"
|
||||
}
|
||||
if balance.Sign() > 0 {
|
||||
return "degraded"
|
||||
}
|
||||
return "unfunded"
|
||||
}
|
||||
|
||||
func proofStatusForLane(key string, proofs map[string]interface{}) string {
|
||||
if proofs == nil {
|
||||
return "proof-pending"
|
||||
}
|
||||
laneProofs, ok := proofs[key].([]interface{})
|
||||
if !ok || len(laneProofs) == 0 {
|
||||
return "proof-pending"
|
||||
}
|
||||
for _, item := range laneProofs {
|
||||
row, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if tx, ok := row["tx_hash"].(string); ok && strings.TrimSpace(tx) != "" {
|
||||
return "proof-recorded"
|
||||
}
|
||||
}
|
||||
return "proof-pending"
|
||||
}
|
||||
|
||||
func readProofTransfersJSON() map[string]interface{} {
|
||||
path := strings.TrimSpace(os.Getenv("MISSION_CONTROL_PROOF_TRANSFERS_JSON"))
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil || len(b) == 0 {
|
||||
return map[string]interface{}{"error": "unreadable or empty", "path": path}
|
||||
}
|
||||
if len(b) > 512*1024 {
|
||||
return map[string]interface{}{"error": "file too large", "path": path}
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(b, &payload); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "path": path}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func probeBridgeContract(ctx context.Context, rpcURL, linkToken, bridgeAddress, minLinkWei string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"bridge": strings.TrimSpace(bridgeAddress),
|
||||
}
|
||||
if rpcURL == "" {
|
||||
result["status"] = "unknown"
|
||||
result["error"] = "rpc unavailable"
|
||||
return result
|
||||
}
|
||||
if linkToken == "" || bridgeAddress == "" {
|
||||
result["status"] = "unknown"
|
||||
result["error"] = "missing link token or bridge address"
|
||||
return result
|
||||
}
|
||||
balance, err := erc20BalanceOf(ctx, rpcURL, linkToken, bridgeAddress)
|
||||
if err != nil {
|
||||
result["status"] = "unknown"
|
||||
result["error"] = err.Error()
|
||||
return result
|
||||
}
|
||||
result["link_balance_wei"] = balance
|
||||
result["status"] = bridgeFundingStatus(balance, minLinkWei)
|
||||
return result
|
||||
}
|
||||
|
||||
func aggregateLaneStatus(weth9Status, weth10Status, proofStatus string) string {
|
||||
statuses := []string{weth9Status, weth10Status}
|
||||
hasUnfunded := false
|
||||
hasDegraded := false
|
||||
hasUnknown := false
|
||||
for _, status := range statuses {
|
||||
switch status {
|
||||
case "unfunded":
|
||||
hasUnfunded = true
|
||||
case "degraded":
|
||||
hasDegraded = true
|
||||
case "unknown":
|
||||
hasUnknown = true
|
||||
}
|
||||
}
|
||||
if hasUnfunded {
|
||||
return "unfunded"
|
||||
}
|
||||
if hasDegraded {
|
||||
return "degraded"
|
||||
}
|
||||
if hasUnknown {
|
||||
return "unknown"
|
||||
}
|
||||
if proofStatus == "proof-pending" {
|
||||
return "proof-pending"
|
||||
}
|
||||
return "funded"
|
||||
}
|
||||
|
||||
func BuildBridgeLaneHealth(ctx context.Context) (map[string]interface{}, map[string]interface{}) {
|
||||
cfg := loadBridgeLanesConfig()
|
||||
minLinkWei := strings.TrimSpace(cfg.MinLinkWei)
|
||||
if minLinkWei == "" {
|
||||
minLinkWei = "1000000000000000000"
|
||||
}
|
||||
|
||||
proofPayload := readProofTransfersJSON()
|
||||
proofByLane := map[string]interface{}{}
|
||||
if proofPayload != nil {
|
||||
if lanes, ok := proofPayload["lanes"].(map[string]interface{}); ok {
|
||||
proofByLane = lanes
|
||||
}
|
||||
}
|
||||
|
||||
lanes := make([]map[string]interface{}, 0, len(cfg.Lanes))
|
||||
for _, def := range cfg.Lanes {
|
||||
rpcURL := resolveLaneRPC(def)
|
||||
weth9 := probeBridgeContract(ctx, rpcURL, def.LinkToken, def.WETH9Bridge, minLinkWei)
|
||||
weth10 := probeBridgeContract(ctx, rpcURL, def.LinkToken, def.WETH10Bridge, minLinkWei)
|
||||
weth9Status, _ := weth9["status"].(string)
|
||||
weth10Status, _ := weth10["status"].(string)
|
||||
proofStatus := proofStatusForLane(def.Key, proofByLane)
|
||||
|
||||
lanes = append(lanes, map[string]interface{}{
|
||||
"key": def.Key,
|
||||
"chain_name": def.ChainName,
|
||||
"chain_id": def.ChainID,
|
||||
"config_ready": def.ConfigReady,
|
||||
"link_token": def.LinkToken,
|
||||
"status": aggregateLaneStatus(weth9Status, weth10Status, proofStatus),
|
||||
"proof_status": proofStatus,
|
||||
"weth9": weth9,
|
||||
"weth10": weth10,
|
||||
"rpc_endpoint": redactRPCOrigin(rpcURL),
|
||||
})
|
||||
}
|
||||
|
||||
laneHealth := map[string]interface{}{
|
||||
"updated_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"min_link_wei": minLinkWei,
|
||||
"lanes": lanes,
|
||||
}
|
||||
return laneHealth, proofPayload
|
||||
}
|
||||
61
backend/api/track1/bridge_lanes_default.json
Normal file
61
backend/api/track1/bridge_lanes_default.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"updated": "2026-05-23",
|
||||
"min_link_wei": "1000000000000000000",
|
||||
"lanes": [
|
||||
{
|
||||
"key": "chain138",
|
||||
"chain_name": "Defi Oracle Meta Mainnet (138)",
|
||||
"chain_id": 138,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["RPC_URL", "RPC_URL_138"],
|
||||
"rpc_default": "http://192.168.11.211:8545",
|
||||
"link_token": "0xb7721dd53a8c629d9f1ba31a5819afe250002b03",
|
||||
"weth9_bridge": "0xcacfd227A040002e49e2e01626363071324f820a",
|
||||
"weth10_bridge": "0xe0E93247376aa097dB308B92e6Ba36bA015535D0"
|
||||
},
|
||||
{
|
||||
"key": "gnosis",
|
||||
"chain_name": "Gnosis (100)",
|
||||
"chain_id": 100,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["GNOSIS_RPC", "GNOSIS_MAINNET_RPC", "GNOSIS_RPC_URL"],
|
||||
"rpc_default": "https://rpc.gnosischain.com",
|
||||
"link_token": "0xE2e73A1c69ecF83F464EFCE6A5be353a37cA09b2",
|
||||
"weth9_bridge": "0xc8656F24488cb90c452058da92d1a25BA464eaAE",
|
||||
"weth10_bridge": "0xa846aeAD3071df1b6439d5D813156aCE7C2c1DA1"
|
||||
},
|
||||
{
|
||||
"key": "cronos",
|
||||
"chain_name": "Cronos (25)",
|
||||
"chain_id": 25,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CRONOS_RPC", "CRONOS_RPC_URL", "CRONOS_MAINNET_RPC"],
|
||||
"rpc_default": "https://evm.cronos.org",
|
||||
"link_token": "0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85",
|
||||
"weth9_bridge": "0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab",
|
||||
"weth10_bridge": "0x105F8A15b819948a89153505762444Ee9f324684"
|
||||
},
|
||||
{
|
||||
"key": "celo",
|
||||
"chain_name": "Celo (42220)",
|
||||
"chain_id": 42220,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CELO_RPC", "CELO_MAINNET_RPC"],
|
||||
"rpc_default": "https://forno.celo.org",
|
||||
"link_token": "0xd07294e6E917e07dfDcee882dd1e2565085C2ae0",
|
||||
"weth9_bridge": "0xAb57BF30F1354CA0590af22D8974c7f24DB2DbD7",
|
||||
"weth10_bridge": "0xa780ef19A041745d353c9432f2a7f5A241335ffE"
|
||||
},
|
||||
{
|
||||
"key": "wemix",
|
||||
"chain_name": "Wemix (1111)",
|
||||
"chain_id": 1111,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["WEMIX_RPC", "WEMIX_MAINNET_RPC"],
|
||||
"rpc_default": "https://api.wemix.com",
|
||||
"link_token": "0x80f1FcdC96B55e459BF52b998aBBE2c364935d69",
|
||||
"weth9_bridge": "0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04",
|
||||
"weth10_bridge": "0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
backend/api/track1/bridge_lanes_test.go
Normal file
37
backend/api/track1/bridge_lanes_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridgeFundingStatus(t *testing.T) {
|
||||
require.Equal(t, "funded", bridgeFundingStatus("2000000000000000000", "1000000000000000000"))
|
||||
require.Equal(t, "degraded", bridgeFundingStatus("500000000000000000", "1000000000000000000"))
|
||||
require.Equal(t, "unfunded", bridgeFundingStatus("0", "1000000000000000000"))
|
||||
}
|
||||
|
||||
func TestAggregateLaneStatus(t *testing.T) {
|
||||
require.Equal(t, "unfunded", aggregateLaneStatus("unfunded", "funded", "proof-recorded"))
|
||||
require.Equal(t, "degraded", aggregateLaneStatus("degraded", "funded", "proof-recorded"))
|
||||
require.Equal(t, "proof-pending", aggregateLaneStatus("funded", "funded", "proof-pending"))
|
||||
require.Equal(t, "funded", aggregateLaneStatus("funded", "funded", "proof-recorded"))
|
||||
}
|
||||
|
||||
func TestProofStatusForLane(t *testing.T) {
|
||||
proofs := map[string]interface{}{
|
||||
"gnosis": []interface{}{
|
||||
map[string]interface{}{"tx_hash": "0xabc"},
|
||||
},
|
||||
}
|
||||
require.Equal(t, "proof-recorded", proofStatusForLane("gnosis", proofs))
|
||||
require.Equal(t, "proof-pending", proofStatusForLane("cronos", proofs))
|
||||
}
|
||||
|
||||
func TestLoadBridgeLanesConfigDefault(t *testing.T) {
|
||||
t.Setenv("MISSION_CONTROL_BRIDGE_LANES_JSON", "")
|
||||
cfg := loadBridgeLanesConfig()
|
||||
require.NotEmpty(t, cfg.Lanes)
|
||||
require.NotEmpty(t, cfg.MinLinkWei)
|
||||
}
|
||||
@@ -30,6 +30,14 @@ func TestResolveBridgeDeliveryModeMixedWhenTransactionVisibilityStale(t *testing
|
||||
require.Equal(t, "bridge_monitoring_and_homepage", got.Scope)
|
||||
}
|
||||
|
||||
func TestResolveBridgeDeliveryModeMixedWhenQuietChain(t *testing.T) {
|
||||
diagnostics := &freshness.Diagnostics{
|
||||
ActivityState: "quiet_chain",
|
||||
}
|
||||
got := resolveBridgeDeliveryMode(false, diagnostics, freshness.CompletenessComplete)
|
||||
require.Equal(t, "live", got.Kind)
|
||||
}
|
||||
|
||||
func TestIsStaleTransactionVisibility(t *testing.T) {
|
||||
require.True(t, isStaleTransactionVisibility(&freshness.Diagnostics{ActivityState: "fresh_head_stale_transaction_visibility"}))
|
||||
require.False(t, isStaleTransactionVisibility(&freshness.Diagnostics{ActivityState: "healthy"}))
|
||||
|
||||
@@ -198,6 +198,12 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
}
|
||||
}
|
||||
}
|
||||
if laneHealth, proofTransfers := BuildBridgeLaneHealth(ctx); laneHealth != nil {
|
||||
data["bridge_lanes"] = laneHealth
|
||||
if proofTransfers != nil {
|
||||
data["proof_transfers"] = proofTransfers
|
||||
}
|
||||
}
|
||||
if mode, ok := data["mode"].(map[string]interface{}); ok {
|
||||
if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
|
||||
var diagnostics *freshness.Diagnostics
|
||||
|
||||
61
config/explorer-bridge-lanes.v1.json
Normal file
61
config/explorer-bridge-lanes.v1.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"updated": "2026-05-23",
|
||||
"min_link_wei": "1000000000000000000",
|
||||
"lanes": [
|
||||
{
|
||||
"key": "chain138",
|
||||
"chain_name": "Defi Oracle Meta Mainnet (138)",
|
||||
"chain_id": 138,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["RPC_URL", "RPC_URL_138"],
|
||||
"rpc_default": "http://192.168.11.211:8545",
|
||||
"link_token": "0xb7721dd53a8c629d9f1ba31a5819afe250002b03",
|
||||
"weth9_bridge": "0xcacfd227A040002e49e2e01626363071324f820a",
|
||||
"weth10_bridge": "0xe0E93247376aa097dB308B92e6Ba36bA015535D0"
|
||||
},
|
||||
{
|
||||
"key": "gnosis",
|
||||
"chain_name": "Gnosis (100)",
|
||||
"chain_id": 100,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["GNOSIS_RPC", "GNOSIS_MAINNET_RPC", "GNOSIS_RPC_URL"],
|
||||
"rpc_default": "https://rpc.gnosischain.com",
|
||||
"link_token": "0xE2e73A1c69ecF83F464EFCE6A5be353a37cA09b2",
|
||||
"weth9_bridge": "0xc8656F24488cb90c452058da92d1a25BA464eaAE",
|
||||
"weth10_bridge": "0xa846aeAD3071df1b6439d5D813156aCE7C2c1DA1"
|
||||
},
|
||||
{
|
||||
"key": "cronos",
|
||||
"chain_name": "Cronos (25)",
|
||||
"chain_id": 25,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CRONOS_RPC", "CRONOS_RPC_URL", "CRONOS_MAINNET_RPC"],
|
||||
"rpc_default": "https://evm.cronos.org",
|
||||
"link_token": "0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85",
|
||||
"weth9_bridge": "0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab",
|
||||
"weth10_bridge": "0x105F8A15b819948a89153505762444Ee9f324684"
|
||||
},
|
||||
{
|
||||
"key": "celo",
|
||||
"chain_name": "Celo (42220)",
|
||||
"chain_id": 42220,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CELO_RPC", "CELO_MAINNET_RPC"],
|
||||
"rpc_default": "https://forno.celo.org",
|
||||
"link_token": "0xd07294e6E917e07dfDcee882dd1e2565085C2ae0",
|
||||
"weth9_bridge": "0xAb57BF30F1354CA0590af22D8974c7f24DB2DbD7",
|
||||
"weth10_bridge": "0xa780ef19A041745d353c9432f2a7f5A241335ffE"
|
||||
},
|
||||
{
|
||||
"key": "wemix",
|
||||
"chain_name": "Wemix (1111)",
|
||||
"chain_id": 1111,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["WEMIX_RPC", "WEMIX_MAINNET_RPC"],
|
||||
"rpc_default": "https://api.wemix.com",
|
||||
"link_token": "0x80f1FcdC96B55e459BF52b998aBBE2c364935d69",
|
||||
"weth9_bridge": "0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04",
|
||||
"weth10_bridge": "0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08"
|
||||
}
|
||||
]
|
||||
}
|
||||
107
frontend/src/components/explorer/BridgeLaneHealthPanel.tsx
Normal file
107
frontend/src/components/explorer/BridgeLaneHealthPanel.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import type { MissionControlBridgeLane, MissionControlBridgeLaneHealth } from '@/services/api/missionControl'
|
||||
|
||||
function laneTone(status?: string | null): 'success' | 'warning' | 'info' | 'neutral' {
|
||||
switch (String(status || '').toLowerCase()) {
|
||||
case 'funded':
|
||||
case 'proof-recorded':
|
||||
return 'success'
|
||||
case 'degraded':
|
||||
case 'proof-pending':
|
||||
return 'warning'
|
||||
case 'unfunded':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
function formatLinkBalance(wei?: string | null): string {
|
||||
if (!wei) return 'Unknown'
|
||||
try {
|
||||
const value = BigInt(wei)
|
||||
const whole = value / 10n ** 18n
|
||||
const fractional = (value % 10n ** 18n).toString().padStart(18, '0').slice(0, 4).replace(/0+$/, '')
|
||||
return fractional ? `${whole}.${fractional} LINK` : `${whole} LINK`
|
||||
} catch {
|
||||
return wei
|
||||
}
|
||||
}
|
||||
|
||||
export default function BridgeLaneHealthPanel({
|
||||
laneHealth,
|
||||
className = '',
|
||||
}: {
|
||||
laneHealth?: MissionControlBridgeLaneHealth | null
|
||||
className?: string
|
||||
}) {
|
||||
const lanes = laneHealth?.lanes || []
|
||||
if (lanes.length === 0) return null
|
||||
|
||||
const normalizedClassName = className ? ` ${className}` : ''
|
||||
|
||||
return (
|
||||
<Card title="Config-ready lane health" className={`mb-8${normalizedClassName}`}>
|
||||
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
LINK balances are read from each remote CCIP bridge contract. Proof-transfer status comes from operator-recorded CCIP message hashes when available.
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Operator runbook: fund LINK with{' '}
|
||||
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">fund-ccip-bridges-with-link.sh</code>
|
||||
{' '}· lane probe{' '}
|
||||
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">probe-bridge-lane-link-balances.sh</code>
|
||||
{' '}· routing reference{' '}
|
||||
<Link href="/docs/public-api-access" className="text-primary-600 hover:underline">
|
||||
public API access
|
||||
</Link>
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
||||
<th className="py-2 pr-4">Lane</th>
|
||||
<th className="py-2 pr-4">Overall</th>
|
||||
<th className="py-2 pr-4">Proof</th>
|
||||
<th className="py-2 pr-4">WETH9 bridge</th>
|
||||
<th className="py-2 pr-4">WETH10 bridge</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lanes.map((lane: MissionControlBridgeLane) => (
|
||||
<tr key={lane.key} className="border-b border-gray-100 align-top last:border-0 dark:border-gray-800">
|
||||
<td className="py-3 pr-4">
|
||||
<div className="font-medium text-gray-900 dark:text-white">{lane.chain_name || lane.key}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">chain {lane.chain_id ?? '?'}</div>
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<EntityBadge label={lane.status || 'unknown'} tone={laneTone(lane.status)} />
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<EntityBadge label={lane.proof_status || 'proof-pending'} tone={laneTone(lane.proof_status)} />
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<div className="font-mono text-xs text-gray-700 dark:text-gray-300">{lane.weth9?.bridge || '—'}</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<EntityBadge label={lane.weth9?.status || 'unknown'} tone={laneTone(lane.weth9?.status)} className="px-2 py-0.5 text-[11px]" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">{formatLinkBalance(lane.weth9?.link_balance_wei)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<div className="font-mono text-xs text-gray-700 dark:text-gray-300">{lane.weth10?.bridge || '—'}</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<EntityBadge label={lane.weth10?.status || 'unknown'} tone={laneTone(lane.weth10?.status)} className="px-2 py-0.5 text-[11px]" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">{formatLinkBalance(lane.weth10?.link_balance_wei)}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import { bridgeRoutesApi, normalizeBridgeRouteEntries, type BridgeRoutesResponse } from '@/services/api/bridgeRoutes'
|
||||
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
|
||||
import { HOME_DASHBOARD_REFRESH_MS } from '@/utils/featuredTokens'
|
||||
import BridgeLaneHealthPanel from '@/components/explorer/BridgeLaneHealthPanel'
|
||||
import OperationsSurfaceNav from './OperationsSurfaceNav'
|
||||
import OperationsActionGrid from './OperationsActionGrid'
|
||||
|
||||
@@ -460,6 +461,8 @@ export default function BridgeMonitoringPage({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<BridgeLaneHealthPanel laneHealth={bridgeStatus?.data?.bridge_lanes} />
|
||||
|
||||
{routeEntries.length > 0 ? (
|
||||
<Card title="CCIP route catalog" className="mb-8">
|
||||
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
|
||||
@@ -10,6 +10,11 @@ const docsCards = [
|
||||
href: '/docs/posture-glossary',
|
||||
description: 'First-read definitions for x402, ISO-20022, forward-canonical, cW public-network, and related explorer badges.',
|
||||
},
|
||||
{
|
||||
title: 'Config compatibility keys',
|
||||
href: '/docs/posture-glossary#transportactive-config-compatibility',
|
||||
description: 'Methodology for public /config compatibility keys (transportActive, forward-canonical) and planned v2 alias mapping.',
|
||||
},
|
||||
{
|
||||
title: 'Public API access',
|
||||
href: '/docs/public-api-access',
|
||||
|
||||
@@ -36,7 +36,8 @@ export default function PostureGlossaryDocsPage() {
|
||||
</Card>
|
||||
|
||||
{postureGlossaryTerms.map((term) => (
|
||||
<Card key={term.id} title={term.title}>
|
||||
<div key={term.id} id={term.id === 'transport-active' ? 'transportactive-config-compatibility' : undefined}>
|
||||
<Card title={term.title}>
|
||||
<div className="space-y-3 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
<p>{term.summary}</p>
|
||||
<div className="rounded-xl border border-sky-200 bg-sky-50/70 p-4 text-sky-950 dark:border-sky-900/50 dark:bg-sky-950/20 dark:text-sky-100">
|
||||
@@ -52,6 +53,7 @@ export default function PostureGlossaryDocsPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Card title="Related references">
|
||||
|
||||
@@ -365,7 +365,9 @@ export default function TokenDetailPage() {
|
||||
<PostureBadge label="Non-canonical indexed token" tone="warning" />
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6 text-amber-950 dark:text-amber-100">
|
||||
This contract is indexed by Blockscout but is not in the curated Chain 138 token registry. Prefer canonical addresses from the token index for trading, liquidity, and bridge routing.
|
||||
This contract is indexed by Blockscout but is not in the curated Chain 138 token registry. Prefer canonical addresses from the{' '}
|
||||
<Link href="/tokens" className="font-medium text-primary-600 hover:underline">token index</Link>
|
||||
{' '}and the posture glossary for trading, liquidity, and bridge routing.
|
||||
</p>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
@@ -95,6 +95,32 @@ export interface MissionControlSubsystemStatus {
|
||||
completeness?: string | null
|
||||
}
|
||||
|
||||
export interface MissionControlBridgeLaneContract {
|
||||
bridge?: string
|
||||
link_balance_wei?: string
|
||||
status?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface MissionControlBridgeLane {
|
||||
key: string
|
||||
chain_name?: string
|
||||
chain_id?: number
|
||||
config_ready?: boolean
|
||||
link_token?: string
|
||||
status?: string
|
||||
proof_status?: string
|
||||
weth9?: MissionControlBridgeLaneContract
|
||||
weth10?: MissionControlBridgeLaneContract
|
||||
rpc_endpoint?: string
|
||||
}
|
||||
|
||||
export interface MissionControlBridgeLaneHealth {
|
||||
updated_at?: string
|
||||
min_link_wei?: string
|
||||
lanes?: MissionControlBridgeLane[]
|
||||
}
|
||||
|
||||
export interface MissionControlBridgeStatusResponse {
|
||||
data?: {
|
||||
status?: string
|
||||
@@ -112,6 +138,8 @@ export interface MissionControlBridgeStatusResponse {
|
||||
chains?: Record<string, MissionControlChainStatus>
|
||||
ccip_relay?: MissionControlRelayPayload
|
||||
ccip_relays?: Record<string, MissionControlRelayPayload>
|
||||
bridge_lanes?: MissionControlBridgeLaneHealth
|
||||
proof_transfers?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@ echo "✅ Docs bundle prepared"
|
||||
echo "=== Step 3: Upload artifacts ==="
|
||||
scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$TMP_DIR/explorer-config-api" root@"$PROXMOX_HOST":/tmp/explorer-config-api
|
||||
scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$TMP_DIR/explorer-ai-docs.tar.gz" root@"$PROXMOX_HOST":/tmp/explorer-ai-docs.tar.gz
|
||||
PROOF_JSON="$REPO_ROOT/reports/status/bridge-lane-proof-transfers-latest.json"
|
||||
if [ -f "$PROOF_JSON" ]; then
|
||||
scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$PROOF_JSON" root@"$PROXMOX_HOST":/tmp/bridge-lane-proof-transfers.json
|
||||
fi
|
||||
echo "✅ Artifacts uploaded"
|
||||
|
||||
echo "=== Step 4: Install backend, refresh docs, and ensure env ==="
|
||||
@@ -91,8 +95,11 @@ if [ -z "$DB_URL" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
pct exec "$VMID" -- bash -lc 'mkdir -p /opt/explorer-ai-docs /etc/systemd/system/explorer-config-api.service.d'
|
||||
pct exec "$VMID" -- bash -lc 'mkdir -p /opt/explorer-ai-docs /etc/systemd/system/explorer-config-api.service.d /opt/explorer-bridge-status'
|
||||
pct push "$VMID" /tmp/explorer-ai-docs.tar.gz /tmp/explorer-ai-docs.tar.gz --perms 0644
|
||||
if [ -f /tmp/bridge-lane-proof-transfers.json ]; then
|
||||
pct push "$VMID" /tmp/bridge-lane-proof-transfers.json /opt/explorer-bridge-status/proof-transfers.json --perms 0644
|
||||
fi
|
||||
pct push "$VMID" /tmp/explorer-config-api /usr/local/bin/explorer-config-api.new --perms 0755
|
||||
|
||||
pct exec "$VMID" -- env \
|
||||
@@ -181,6 +188,16 @@ else
|
||||
rm -f /etc/systemd/system/explorer-config-api.service.d/walletconnect.conf
|
||||
fi
|
||||
|
||||
cat > /etc/systemd/system/explorer-config-api.service.d/bridge-lanes.conf <<'BRIDGEEOF'
|
||||
[Service]
|
||||
Environment=RPC_URL=http://192.168.11.211:8545
|
||||
Environment="MISSION_CONTROL_EXTRA_RPCS=gnosis|https://rpc.gnosischain.com|100
|
||||
cronos|https://evm.cronos.org|25
|
||||
celo|https://forno.celo.org|42220
|
||||
wemix|https://api.wemix.com|1111"
|
||||
Environment=MISSION_CONTROL_PROOF_TRANSFERS_JSON=/opt/explorer-bridge-status/proof-transfers.json
|
||||
BRIDGEEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart explorer-config-api
|
||||
sleep 2
|
||||
|
||||
@@ -45,12 +45,22 @@ test.describe('Explorer sprint smoke', () => {
|
||||
await expect(page.getByText(/CCIP route catalog/i).first()).toBeVisible({ timeout: 15000 })
|
||||
await expect(page.getByText(/Wemix/i).first()).toBeVisible({ timeout: 15000 })
|
||||
await expect(page.getByText(/Bridge Freshness Context/i).first()).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/Config-ready lane health/i).first()).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/unfunded|funded/i).first()).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('posture glossary doc page loads', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/docs/posture-glossary`, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
||||
await expect(page.getByRole('heading', { name: /Posture glossary/i })).toBeVisible({ timeout: 15000 })
|
||||
await expect(page.getByText(/x402 readiness/i).first()).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/transportActive/i).first()).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('posture glossary drawer opens from docs badges', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/docs/posture-glossary`, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
||||
await page.getByRole('button', { name: /^GRU$/i }).click()
|
||||
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/Methodology/i).first()).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('public API access doc page loads', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user