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>
256 lines
6.8 KiB
Go
256 lines
6.8 KiB
Go
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
|
|
}
|