Files
explorer-monorepo/backend/api/track4/endpoints.go
defiQUG bdae5a9f6e feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths.
- Frontend wallet and liquidity surfaces; MetaMask token list alignment.
- Deployment docs, verification scripts, address inventory updates.

Check: go build ./... under backend/ (pass).
Made-with: Cursor
2026-04-07 23:22:12 -07:00

585 lines
16 KiB
Go

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, &timestamp); 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
}