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
|
||||
|
||||
Reference in New Issue
Block a user