package track1 import ( "context" "encoding/json" "fmt" "math/big" "net/http" "regexp" "strconv" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/explorer/backend/api/freshness" "github.com/explorer/backend/libs/go-rpc-gateway" ) var track1HashPattern = regexp.MustCompile(`^0x[a-fA-F0-9]{64}$`) // Server handles Track 1 endpoints (uses RPC gateway from lib) type Server struct { rpcGateway *gateway.RPCGateway freshnessLoader func(ctx context.Context) (*freshness.Snapshot, *freshness.SummaryCompleteness, *freshness.Sampling, *freshness.Diagnostics, error) } // NewServer creates a new Track 1 server func NewServer( rpcGateway *gateway.RPCGateway, freshnessLoader func(ctx context.Context) (*freshness.Snapshot, *freshness.SummaryCompleteness, *freshness.Sampling, *freshness.Diagnostics, error), ) *Server { return &Server{ rpcGateway: rpcGateway, freshnessLoader: freshnessLoader, } } // HandleLatestBlocks handles GET /api/v1/track1/blocks/latest func (s *Server) HandleLatestBlocks(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } limit := 10 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 { limit = l } } // Get latest block number blockNumResp, err := s.rpcGateway.GetBlockNumber(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "rpc_error", err.Error()) return } blockNumHex, ok := blockNumResp.Result.(string) if !ok { writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block number response") return } // Parse block number blockNum, err := hexToInt(blockNumHex) if err != nil { writeError(w, http.StatusInternalServerError, "parse_error", err.Error()) return } // Fetch blocks blocks := []map[string]interface{}{} for i := 0; i < limit && blockNum-int64(i) >= 0; i++ { blockNumStr := fmt.Sprintf("0x%x", blockNum-int64(i)) blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, false) if err != nil { continue // Skip failed blocks } blockData, ok := blockResp.Result.(map[string]interface{}) if !ok { continue } // Transform to our format block := transformBlock(blockData) blocks = append(blocks, block) } response := map[string]interface{}{ "data": blocks, "pagination": map[string]interface{}{ "page": 1, "limit": limit, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleLatestTransactions handles GET /api/v1/track1/txs/latest func (s *Server) HandleLatestTransactions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } limit := 10 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 { limit = l } } // Get latest block number blockNumResp, err := s.rpcGateway.GetBlockNumber(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "rpc_error", err.Error()) return } blockNumHex, ok := blockNumResp.Result.(string) if !ok { writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block number response") return } blockNum, err := hexToInt(blockNumHex) if err != nil { writeError(w, http.StatusInternalServerError, "parse_error", err.Error()) return } // Fetch transactions from recent blocks transactions := []map[string]interface{}{} for i := 0; i < 20 && len(transactions) < limit && blockNum-int64(i) >= 0; i++ { blockNumStr := fmt.Sprintf("0x%x", blockNum-int64(i)) blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, true) if err != nil { continue } blockData, ok := blockResp.Result.(map[string]interface{}) if !ok { continue } txs, ok := blockData["transactions"].([]interface{}) if !ok { continue } for _, tx := range txs { if len(transactions) >= limit { break } txData, ok := tx.(map[string]interface{}) if !ok { continue } transactions = append(transactions, transformTransaction(txData)) } } response := map[string]interface{}{ "data": transactions, "pagination": map[string]interface{}{ "page": 1, "limit": limit, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleBlockDetail handles GET /api/v1/track1/block/:number func (s *Server) HandleBlockDetail(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/block/") blockNumber, err := strconv.ParseInt(strings.TrimSpace(path), 10, 64) if err != nil || blockNumber < 0 { writeError(w, http.StatusBadRequest, "bad_request", "Invalid block number") return } blockNumStr := fmt.Sprintf("0x%x", blockNumber) blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, false) if err != nil { writeError(w, http.StatusNotFound, "not_found", "Block not found") return } blockData, ok := blockResp.Result.(map[string]interface{}) if !ok { writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block response") return } response := map[string]interface{}{ "data": transformBlock(blockData), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleTransactionDetail handles GET /api/v1/track1/tx/:hash func (s *Server) HandleTransactionDetail(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/tx/") txHash := strings.TrimSpace(path) if !track1HashPattern.MatchString(txHash) { writeError(w, http.StatusBadRequest, "bad_request", "Invalid transaction hash") return } txResp, err := s.rpcGateway.GetTransactionByHash(r.Context(), txHash) if err != nil { writeError(w, http.StatusNotFound, "not_found", "Transaction not found") return } txData, ok := txResp.Result.(map[string]interface{}) if !ok { writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid transaction response") return } response := map[string]interface{}{ "data": transformTransaction(txData), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleAddressBalance handles GET /api/v1/track1/address/:addr/balance func (s *Server) HandleAddressBalance(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/address/") parts := strings.Split(path, "/") if len(parts) < 2 || parts[1] != "balance" { writeError(w, http.StatusBadRequest, "bad_request", "Invalid path") return } address := strings.TrimSpace(parts[0]) if !common.IsHexAddress(address) { writeError(w, http.StatusBadRequest, "bad_request", "Invalid address") return } balanceResp, err := s.rpcGateway.GetBalance(r.Context(), address, "latest") if err != nil { writeError(w, http.StatusInternalServerError, "rpc_error", err.Error()) return } balanceHex, ok := balanceResp.Result.(string) if !ok { writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid balance response") return } balance, err := hexToBigInt(balanceHex) if err != nil { writeError(w, http.StatusInternalServerError, "parse_error", err.Error()) return } response := map[string]interface{}{ "data": map[string]interface{}{ "address": address, "balance": balance.String(), "balance_wei": balance.String(), "balance_ether": weiToEther(balance), }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleBridgeStatus handles GET /api/v1/track1/bridge/status func (s *Server) HandleBridgeStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second) defer cancel() data := s.BuildBridgeStatusData(ctx) response := map[string]interface{}{ "data": data, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func chainStatusFromProbe(p RPCProbeResult) string { if p.OK { return "operational" } return "unreachable" } // Helper functions 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 hexToInt(hex string) (int64, error) { hex = strings.TrimPrefix(hex, "0x") return strconv.ParseInt(hex, 16, 64) } func transformBlock(blockData map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "number": parseHexField(blockData["number"]), "hash": blockData["hash"], "parent_hash": blockData["parentHash"], "timestamp": parseHexTimestamp(blockData["timestamp"]), "transaction_count": countTransactions(blockData["transactions"]), "gas_used": parseHexField(blockData["gasUsed"]), "gas_limit": parseHexField(blockData["gasLimit"]), "miner": blockData["miner"], } } func transformTransaction(txData map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "hash": txData["hash"], "from": txData["from"], "to": txData["to"], "value": txData["value"], "block_number": parseHexField(txData["blockNumber"]), "timestamp": parseHexTimestamp(txData["timestamp"]), } } func parseHexField(field interface{}) interface{} { if str, ok := field.(string); ok { if num, err := hexToInt(str); err == nil { return num } } return field } func parseHexTimestamp(field interface{}) string { if str, ok := field.(string); ok { if num, err := hexToInt(str); err == nil { return time.Unix(num, 0).Format(time.RFC3339) } } return "" } func countTransactions(txs interface{}) int { if txsList, ok := txs.([]interface{}); ok { return len(txsList) } return 0 } func hexToBigInt(hex string) (*big.Int, error) { hex = strings.TrimPrefix(hex, "0x") bigInt := new(big.Int) bigInt, ok := bigInt.SetString(hex, 16) if !ok { return nil, fmt.Errorf("invalid hex number") } return bigInt, nil } func weiToEther(wei *big.Int) string { ether := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18)) return ether.Text('f', 18) }