package track4 import ( "context" "encoding/json" "fmt" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" "github.com/explorer/backend/auth" "github.com/jackc/pgx/v5/pgxpool" ) // Server handles Track 4 endpoints type Server struct { db *pgxpool.Pool roleMgr roleManager chainID int } // NewServer creates a new Track 4 server func NewServer(db *pgxpool.Pool, chainID int) *Server { return &Server{ db: db, roleMgr: auth.NewRoleManager(db), chainID: chainID, } } // HandleBridgeEvents handles GET /api/v1/track4/operator/bridge/events func (s *Server) HandleBridgeEvents(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } operatorAddr, ipAddr, ok := s.requireOperatorAccess(w, r) if !ok { return } events, lastUpdate, err := s.loadBridgeEvents(r.Context(), 100) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } s.roleMgr.LogOperatorEvent(r.Context(), "bridge_events_read", &s.chainID, operatorAddr, "bridge/events", "read", map[string]interface{}{"event_count": len(events)}, ipAddr, r.UserAgent()) controlState := map[string]interface{}{ "paused": nil, "maintenance_mode": nil, "bridge_control_unavailable": true, } if !lastUpdate.IsZero() { controlState["last_update"] = lastUpdate.UTC().Format(time.RFC3339) } response := map[string]interface{}{ "data": map[string]interface{}{ "events": events, "control_state": controlState, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleValidators handles GET /api/v1/track4/operator/validators func (s *Server) HandleValidators(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } operatorAddr, ipAddr, ok := s.requireOperatorAccess(w, r) if !ok { return } validators, err := s.loadValidatorStatus(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } s.roleMgr.LogOperatorEvent(r.Context(), "validators_read", &s.chainID, operatorAddr, "validators", "read", map[string]interface{}{"validator_count": len(validators)}, ipAddr, r.UserAgent()) response := map[string]interface{}{ "data": map[string]interface{}{ "validators": validators, "total_validators": len(validators), "active_validators": len(validators), }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleContracts handles GET /api/v1/track4/operator/contracts func (s *Server) HandleContracts(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } operatorAddr, ipAddr, ok := s.requireOperatorAccess(w, r) if !ok { return } chainID := s.chainID if raw := strings.TrimSpace(r.URL.Query().Get("chain_id")); raw != "" { parsed, err := strconv.Atoi(raw) if err != nil || parsed < 0 { writeError(w, http.StatusBadRequest, "bad_request", "invalid chain_id") return } chainID = parsed } typeFilter := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("type"))) contracts, err := s.loadContractStatus(r.Context(), chainID, typeFilter) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } s.roleMgr.LogOperatorEvent(r.Context(), "contracts_read", &s.chainID, operatorAddr, "contracts", "read", map[string]interface{}{"contract_count": len(contracts), "chain_id": chainID, "type": typeFilter}, ipAddr, r.UserAgent()) response := map[string]interface{}{ "data": map[string]interface{}{ "contracts": contracts, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleProtocolState handles GET /api/v1/track4/operator/protocol-state func (s *Server) HandleProtocolState(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } operatorAddr, ipAddr, ok := s.requireOperatorAccess(w, r) if !ok { return } state, err := s.loadProtocolState(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } s.roleMgr.LogOperatorEvent(r.Context(), "protocol_state_read", &s.chainID, operatorAddr, "protocol/state", "read", map[string]interface{}{}, ipAddr, r.UserAgent()) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"data": state}) } func writeError(w http.ResponseWriter, statusCode int, code, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(map[string]interface{}{ "error": map[string]interface{}{ "code": code, "message": message, }, }) } func (s *Server) requireOperatorAccess(w http.ResponseWriter, r *http.Request) (string, string, bool) { if s.db == nil { writeError(w, http.StatusServiceUnavailable, "service_unavailable", "database not configured") return "", "", false } operatorAddr, _ := r.Context().Value("user_address").(string) operatorAddr = strings.TrimSpace(operatorAddr) if operatorAddr == "" { writeError(w, http.StatusUnauthorized, "unauthorized", "Operator address required") return "", "", false } ipAddr := clientIPAddress(r) whitelisted, err := s.roleMgr.IsIPWhitelisted(r.Context(), operatorAddr, ipAddr) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return "", "", false } if !whitelisted { writeError(w, http.StatusForbidden, "forbidden", "IP address not whitelisted") return "", "", false } return operatorAddr, ipAddr, true } func (s *Server) loadBridgeEvents(ctx context.Context, limit int) ([]map[string]interface{}, time.Time, error) { rows, err := s.db.Query(ctx, ` SELECT event_type, operator_address, target_resource, action, details, COALESCE(ip_address::text, ''), COALESCE(user_agent, ''), timestamp FROM operator_events WHERE (chain_id = $1 OR chain_id IS NULL) AND ( event_type ILIKE '%bridge%' OR target_resource ILIKE 'bridge%' OR target_resource ILIKE '%bridge%' ) ORDER BY timestamp DESC LIMIT $2 `, s.chainID, limit) if err != nil { return nil, time.Time{}, fmt.Errorf("failed to query bridge events: %w", err) } defer rows.Close() events := make([]map[string]interface{}, 0, limit) var latest time.Time for rows.Next() { var eventType, operatorAddress, targetResource, action, ipAddress, userAgent string var detailsBytes []byte var timestamp time.Time if err := rows.Scan(&eventType, &operatorAddress, &targetResource, &action, &detailsBytes, &ipAddress, &userAgent, ×tamp); err != nil { return nil, time.Time{}, fmt.Errorf("failed to scan bridge event: %w", err) } details := map[string]interface{}{} if len(detailsBytes) > 0 && string(detailsBytes) != "null" { _ = json.Unmarshal(detailsBytes, &details) } if latest.IsZero() { latest = timestamp } events = append(events, map[string]interface{}{ "event_type": eventType, "operator_address": operatorAddress, "target_resource": targetResource, "action": action, "details": details, "ip_address": ipAddress, "user_agent": userAgent, "timestamp": timestamp.UTC().Format(time.RFC3339), }) } return events, latest, rows.Err() } func (s *Server) loadValidatorStatus(ctx context.Context) ([]map[string]interface{}, error) { rows, err := s.db.Query(ctx, ` SELECT r.address, COALESCE(r.roles, '{}'), COALESCE(oe.last_seen, r.updated_at, r.approved_at), r.track_level FROM operator_roles r LEFT JOIN LATERAL ( SELECT MAX(timestamp) AS last_seen FROM operator_events WHERE operator_address = r.address ) oe ON TRUE WHERE r.approved = TRUE AND r.track_level >= 4 ORDER BY COALESCE(oe.last_seen, r.updated_at, r.approved_at) DESC NULLS LAST, r.address `) if err != nil { return nil, fmt.Errorf("failed to query validator status: %w", err) } defer rows.Close() validators := make([]map[string]interface{}, 0) for rows.Next() { var address string var roles []string var lastSeen time.Time var trackLevel int if err := rows.Scan(&address, &roles, &lastSeen, &trackLevel); err != nil { return nil, fmt.Errorf("failed to scan validator row: %w", err) } roleScope := "operator" if inferred := inferOperatorScope(roles); inferred != "" { roleScope = inferred } row := map[string]interface{}{ "address": address, "status": "active", "stake": nil, "uptime": nil, "last_block": nil, "track_level": trackLevel, "roles": roles, "role_scope": roleScope, } if !lastSeen.IsZero() { row["last_seen"] = lastSeen.UTC().Format(time.RFC3339) } validators = append(validators, row) } return validators, rows.Err() } type contractRegistryEntry struct { Address string ChainID int Name string Type string } func (s *Server) loadContractStatus(ctx context.Context, chainID int, typeFilter string) ([]map[string]interface{}, error) { type contractRow struct { Name string Status string Compiler string LastVerified *time.Time } dbRows := map[string]contractRow{} rows, err := s.db.Query(ctx, ` SELECT LOWER(address), COALESCE(name, ''), verification_status, compiler_version, verified_at FROM contracts WHERE chain_id = $1 `, chainID) if err != nil { return nil, fmt.Errorf("failed to query contracts: %w", err) } defer rows.Close() for rows.Next() { var address string var row contractRow if err := rows.Scan(&address, &row.Name, &row.Status, &row.Compiler, &row.LastVerified); err != nil { return nil, fmt.Errorf("failed to scan contract row: %w", err) } dbRows[address] = row } if err := rows.Err(); err != nil { return nil, err } registryEntries, err := loadContractRegistry(chainID) if err != nil { registryEntries = nil } seen := map[string]bool{} contracts := make([]map[string]interface{}, 0, len(registryEntries)+len(dbRows)) appendRow := func(address, name, contractType, status, version string, lastVerified *time.Time) { if typeFilter != "" && contractType != typeFilter { return } row := map[string]interface{}{ "address": address, "chain_id": chainID, "type": contractType, "name": name, "status": status, } if version != "" { row["version"] = version } if lastVerified != nil && !lastVerified.IsZero() { row["last_verified"] = lastVerified.UTC().Format(time.RFC3339) } contracts = append(contracts, row) seen[address] = true } for _, entry := range registryEntries { lowerAddress := strings.ToLower(entry.Address) dbRow, ok := dbRows[lowerAddress] status := "registry_only" version := "" name := entry.Name var lastVerified *time.Time if ok { if dbRow.Name != "" { name = dbRow.Name } status = dbRow.Status version = dbRow.Compiler lastVerified = dbRow.LastVerified } appendRow(lowerAddress, name, entry.Type, status, version, lastVerified) } for address, row := range dbRows { if seen[address] { continue } contractType := inferContractType(row.Name) appendRow(address, fallbackString(row.Name, address), contractType, row.Status, row.Compiler, row.LastVerified) } sort.Slice(contracts, func(i, j int) bool { left, _ := contracts[i]["name"].(string) right, _ := contracts[j]["name"].(string) if left == right { return contracts[i]["address"].(string) < contracts[j]["address"].(string) } return left < right }) return contracts, nil } func (s *Server) loadProtocolState(ctx context.Context) (map[string]interface{}, error) { var totalBridged string var activeBridges int var lastBridgeAt *time.Time err := s.db.QueryRow(ctx, ` SELECT COALESCE(SUM(amount)::text, '0'), COUNT(DISTINCT CONCAT(chain_from, ':', chain_to)), MAX(timestamp) FROM analytics_bridge_history WHERE status ILIKE 'success%' AND (chain_from = $1 OR chain_to = $1) `, s.chainID).Scan(&totalBridged, &activeBridges, &lastBridgeAt) if err != nil { return nil, fmt.Errorf("failed to query protocol state: %w", err) } registryEntries, _ := loadContractRegistry(s.chainID) bridgeEnabled := activeBridges > 0 if !bridgeEnabled { for _, entry := range registryEntries { if entry.Type == "bridge" { bridgeEnabled = true break } } } protocolVersion := strings.TrimSpace(os.Getenv("EXPLORER_PROTOCOL_VERSION")) if protocolVersion == "" { protocolVersion = strings.TrimSpace(os.Getenv("PROTOCOL_VERSION")) } if protocolVersion == "" { protocolVersion = "unknown" } data := map[string]interface{}{ "protocol_version": protocolVersion, "chain_id": s.chainID, "config": map[string]interface{}{ "bridge_enabled": bridgeEnabled, "max_transfer_amount": nil, "max_transfer_amount_unavailable": true, "fee_structure": nil, }, "state": map[string]interface{}{ "total_locked": nil, "total_locked_unavailable": true, "total_bridged": totalBridged, "active_bridges": activeBridges, }, } if lastBridgeAt != nil && !lastBridgeAt.IsZero() { data["last_updated"] = lastBridgeAt.UTC().Format(time.RFC3339) } else { data["last_updated"] = time.Now().UTC().Format(time.RFC3339) } return data, nil } func loadContractRegistry(chainID int) ([]contractRegistryEntry, error) { chainKey := strconv.Itoa(chainID) candidates := []string{} if env := strings.TrimSpace(os.Getenv("SMART_CONTRACTS_MASTER_JSON")); env != "" { candidates = append(candidates, env) } candidates = append(candidates, "config/smart-contracts-master.json", "../config/smart-contracts-master.json", "../../config/smart-contracts-master.json", filepath.Join("explorer-monorepo", "config", "smart-contracts-master.json"), ) var raw []byte for _, candidate := range candidates { if strings.TrimSpace(candidate) == "" { continue } body, err := os.ReadFile(candidate) if err == nil && len(body) > 0 { raw = body break } } if len(raw) == 0 { return nil, fmt.Errorf("smart-contracts-master.json not found") } var root struct { Chains map[string]struct { Contracts map[string]string `json:"contracts"` } `json:"chains"` } if err := json.Unmarshal(raw, &root); err != nil { return nil, fmt.Errorf("failed to parse contract registry: %w", err) } chain, ok := root.Chains[chainKey] if !ok { return nil, nil } entries := make([]contractRegistryEntry, 0, len(chain.Contracts)) for name, address := range chain.Contracts { addr := strings.TrimSpace(address) if addr == "" { continue } entries = append(entries, contractRegistryEntry{ Address: addr, ChainID: chainID, Name: name, Type: inferContractType(name), }) } sort.Slice(entries, func(i, j int) bool { if entries[i].Name == entries[j].Name { return strings.ToLower(entries[i].Address) < strings.ToLower(entries[j].Address) } return entries[i].Name < entries[j].Name }) return entries, nil } func inferOperatorScope(roles []string) string { for _, role := range roles { lower := strings.ToLower(role) switch { case strings.Contains(lower, "validator"): return "validator" case strings.Contains(lower, "sequencer"): return "sequencer" case strings.Contains(lower, "bridge"): return "bridge" } } return "" } func inferContractType(name string) string { lower := strings.ToLower(name) switch { case strings.Contains(lower, "bridge"): return "bridge" case strings.Contains(lower, "router"): return "router" case strings.Contains(lower, "pool"), strings.Contains(lower, "pmm"), strings.Contains(lower, "amm"): return "liquidity" case strings.Contains(lower, "oracle"): return "oracle" case strings.Contains(lower, "vault"): return "vault" case strings.Contains(lower, "token"), strings.Contains(lower, "weth"), strings.Contains(lower, "cw"), strings.Contains(lower, "usdt"), strings.Contains(lower, "usdc"): return "token" default: return "contract" } } func fallbackString(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value }