Files
explorer-monorepo/backend/api/track1/endpoints.go
defiQUG 0c869f7930 feat(freshness): enhance diagnostics and update snapshot structure
- Introduced a new Diagnostics struct to capture transaction visibility state and activity state.
- Updated BuildSnapshot function to return diagnostics alongside snapshot, completeness, and sampling.
- Enhanced test cases to validate the new diagnostics data.
- Updated frontend components to utilize the new diagnostics information for improved user feedback on freshness context.

This change improves the observability of transaction activity and enhances the user experience by providing clearer insights into the freshness of data.
2026-04-12 18:22:08 -07:00

404 lines
11 KiB
Go

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)
}