package rest import ( "context" "encoding/json" "fmt" "net/http" "time" "github.com/jackc/pgx/v5" ) type explorerStats struct { TotalBlocks int64 `json:"total_blocks"` TotalTransactions int64 `json:"total_transactions"` TotalAddresses int64 `json:"total_addresses"` LatestBlock int64 `json:"latest_block"` } type statsQueryFunc func(ctx context.Context, sql string, args ...any) pgx.Row func loadExplorerStats(ctx context.Context, chainID int, queryRow statsQueryFunc) (explorerStats, error) { var stats explorerStats if err := queryRow(ctx, `SELECT COUNT(*) FROM blocks WHERE chain_id = $1`, chainID, ).Scan(&stats.TotalBlocks); err != nil { return explorerStats{}, fmt.Errorf("query total blocks: %w", err) } if err := queryRow(ctx, `SELECT COUNT(*) FROM transactions WHERE chain_id = $1`, chainID, ).Scan(&stats.TotalTransactions); err != nil { return explorerStats{}, fmt.Errorf("query total transactions: %w", err) } if err := queryRow(ctx, `SELECT COUNT(*) FROM ( SELECT from_address AS address FROM transactions WHERE chain_id = $1 AND from_address IS NOT NULL AND from_address <> '' UNION SELECT to_address AS address FROM transactions WHERE chain_id = $1 AND to_address IS NOT NULL AND to_address <> '' ) unique_addresses`, chainID, ).Scan(&stats.TotalAddresses); err != nil { return explorerStats{}, fmt.Errorf("query total addresses: %w", err) } if err := queryRow(ctx, `SELECT COALESCE(MAX(number), 0) FROM blocks WHERE chain_id = $1`, chainID, ).Scan(&stats.LatestBlock); err != nil { return explorerStats{}, fmt.Errorf("query latest block: %w", err) } return stats, nil } // handleStats handles GET /api/v2/stats func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeMethodNotAllowed(w) return } if !s.requireDB(w) { return } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() stats, err := loadExplorerStats(ctx, s.chainID, s.db.QueryRow) if err != nil { writeError(w, http.StatusServiceUnavailable, "service_unavailable", "explorer stats are temporarily unavailable") return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) }