From 228fa0eef6c5df1f0451e445968185065dad100f Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sat, 23 May 2026 04:21:44 -0700 Subject: [PATCH] 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 --- backend/api/track1/bridge_lanes.go | 255 ++++++++++++++++++ backend/api/track1/bridge_lanes_default.json | 61 +++++ backend/api/track1/bridge_lanes_test.go | 37 +++ backend/api/track1/bridge_mode_test.go | 8 + backend/api/track1/bridge_status_data.go | 6 + config/explorer-bridge-lanes.v1.json | 61 +++++ .../explorer/BridgeLaneHealthPanel.tsx | 107 ++++++++ .../explorer/BridgeMonitoringPage.tsx | 3 + frontend/src/pages/docs/index.tsx | 5 + frontend/src/pages/docs/posture-glossary.tsx | 4 +- frontend/src/pages/tokens/[address].tsx | 4 +- frontend/src/services/api/missionControl.ts | 28 ++ scripts/deploy-explorer-ai-to-vmid5000.sh | 19 +- scripts/e2e-sprint-smoke.spec.ts | 10 + 14 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 backend/api/track1/bridge_lanes.go create mode 100644 backend/api/track1/bridge_lanes_default.json create mode 100644 backend/api/track1/bridge_lanes_test.go create mode 100644 config/explorer-bridge-lanes.v1.json create mode 100644 frontend/src/components/explorer/BridgeLaneHealthPanel.tsx diff --git a/backend/api/track1/bridge_lanes.go b/backend/api/track1/bridge_lanes.go new file mode 100644 index 0000000..fcb8dfe --- /dev/null +++ b/backend/api/track1/bridge_lanes.go @@ -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 +} diff --git a/backend/api/track1/bridge_lanes_default.json b/backend/api/track1/bridge_lanes_default.json new file mode 100644 index 0000000..0a68358 --- /dev/null +++ b/backend/api/track1/bridge_lanes_default.json @@ -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" + } + ] +} diff --git a/backend/api/track1/bridge_lanes_test.go b/backend/api/track1/bridge_lanes_test.go new file mode 100644 index 0000000..7137fad --- /dev/null +++ b/backend/api/track1/bridge_lanes_test.go @@ -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) +} diff --git a/backend/api/track1/bridge_mode_test.go b/backend/api/track1/bridge_mode_test.go index f6d7b3f..4b22d01 100644 --- a/backend/api/track1/bridge_mode_test.go +++ b/backend/api/track1/bridge_mode_test.go @@ -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"})) diff --git a/backend/api/track1/bridge_status_data.go b/backend/api/track1/bridge_status_data.go index d66f9e0..f043670 100644 --- a/backend/api/track1/bridge_status_data.go +++ b/backend/api/track1/bridge_status_data.go @@ -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 diff --git a/config/explorer-bridge-lanes.v1.json b/config/explorer-bridge-lanes.v1.json new file mode 100644 index 0000000..0a68358 --- /dev/null +++ b/config/explorer-bridge-lanes.v1.json @@ -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" + } + ] +} diff --git a/frontend/src/components/explorer/BridgeLaneHealthPanel.tsx b/frontend/src/components/explorer/BridgeLaneHealthPanel.tsx new file mode 100644 index 0000000..e8a70ce --- /dev/null +++ b/frontend/src/components/explorer/BridgeLaneHealthPanel.tsx @@ -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 ( + +

+ LINK balances are read from each remote CCIP bridge contract. Proof-transfer status comes from operator-recorded CCIP message hashes when available. +

+

+ Operator runbook: fund LINK with{' '} + fund-ccip-bridges-with-link.sh + {' '}· lane probe{' '} + probe-bridge-lane-link-balances.sh + {' '}· routing reference{' '} + + public API access + +

+
+ + + + + + + + + + + + {lanes.map((lane: MissionControlBridgeLane) => ( + + + + + + + + ))} + +
LaneOverallProofWETH9 bridgeWETH10 bridge
+
{lane.chain_name || lane.key}
+
chain {lane.chain_id ?? '?'}
+
+ + + + +
{lane.weth9?.bridge || '—'}
+
+ + {formatLinkBalance(lane.weth9?.link_balance_wei)} +
+
+
{lane.weth10?.bridge || '—'}
+
+ + {formatLinkBalance(lane.weth10?.link_balance_wei)} +
+
+
+
+ ) +} diff --git a/frontend/src/components/explorer/BridgeMonitoringPage.tsx b/frontend/src/components/explorer/BridgeMonitoringPage.tsx index 1566eb2..49bda20 100644 --- a/frontend/src/components/explorer/BridgeMonitoringPage.tsx +++ b/frontend/src/components/explorer/BridgeMonitoringPage.tsx @@ -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({ ))} + + {routeEntries.length > 0 ? (

diff --git a/frontend/src/pages/docs/index.tsx b/frontend/src/pages/docs/index.tsx index ab8ddc2..b980bd5 100644 --- a/frontend/src/pages/docs/index.tsx +++ b/frontend/src/pages/docs/index.tsx @@ -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', diff --git a/frontend/src/pages/docs/posture-glossary.tsx b/frontend/src/pages/docs/posture-glossary.tsx index 0e54760..0d26488 100644 --- a/frontend/src/pages/docs/posture-glossary.tsx +++ b/frontend/src/pages/docs/posture-glossary.tsx @@ -36,7 +36,8 @@ export default function PostureGlossaryDocsPage() { {postureGlossaryTerms.map((term) => ( - +

+

{term.summary}

@@ -52,6 +53,7 @@ export default function PostureGlossaryDocsPage() { ) : null}
+
))} diff --git a/frontend/src/pages/tokens/[address].tsx b/frontend/src/pages/tokens/[address].tsx index 61e0329..07fc584 100644 --- a/frontend/src/pages/tokens/[address].tsx +++ b/frontend/src/pages/tokens/[address].tsx @@ -365,7 +365,9 @@ export default function TokenDetailPage() {

- 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{' '} + token index + {' '}and the posture glossary for trading, liquidity, and bridge routing.

) : null} diff --git a/frontend/src/services/api/missionControl.ts b/frontend/src/services/api/missionControl.ts index 63a3df7..7f4ace8 100644 --- a/frontend/src/services/api/missionControl.ts +++ b/frontend/src/services/api/missionControl.ts @@ -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 ccip_relay?: MissionControlRelayPayload ccip_relays?: Record + bridge_lanes?: MissionControlBridgeLaneHealth + proof_transfers?: Record } } diff --git a/scripts/deploy-explorer-ai-to-vmid5000.sh b/scripts/deploy-explorer-ai-to-vmid5000.sh index a4904c5..0155feb 100644 --- a/scripts/deploy-explorer-ai-to-vmid5000.sh +++ b/scripts/deploy-explorer-ai-to-vmid5000.sh @@ -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 diff --git a/scripts/e2e-sprint-smoke.spec.ts b/scripts/e2e-sprint-smoke.spec.ts index cfd08ba..9c9adce 100644 --- a/scripts/e2e-sprint-smoke.spec.ts +++ b/scripts/e2e-sprint-smoke.spec.ts @@ -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 }) => {