package track3 import ( "encoding/json" "net/http" "strconv" "strings" "time" "github.com/explorer/backend/analytics" "github.com/jackc/pgx/v5/pgxpool" ) // Server handles Track 3 endpoints type Server struct { db *pgxpool.Pool flowTracker *analytics.FlowTracker bridgeAnalytics *analytics.BridgeAnalytics tokenDist *analytics.TokenDistribution riskAnalyzer *analytics.AddressRiskAnalyzer chainID int } // NewServer creates a new Track 3 server func NewServer(db *pgxpool.Pool, chainID int) *Server { return &Server{ db: db, flowTracker: analytics.NewFlowTracker(db, chainID), bridgeAnalytics: analytics.NewBridgeAnalytics(db), tokenDist: analytics.NewTokenDistribution(db, chainID), riskAnalyzer: analytics.NewAddressRiskAnalyzer(db, chainID), chainID: chainID, } } // HandleFlows handles GET /api/v1/track3/analytics/flows func (s *Server) HandleFlows(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") to := r.URL.Query().Get("to") token := r.URL.Query().Get("token") limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 || limit > 200 { limit = 50 } var startDate, endDate *time.Time if startStr := r.URL.Query().Get("start_date"); startStr != "" { if t, err := time.Parse(time.RFC3339, startStr); err == nil { startDate = &t } } if endStr := r.URL.Query().Get("end_date"); endStr != "" { if t, err := time.Parse(time.RFC3339, endStr); err == nil { endDate = &t } } flows, err := s.flowTracker.GetFlows(r.Context(), from, to, token, startDate, endDate, limit) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } response := map[string]interface{}{ "data": map[string]interface{}{ "flows": flows, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleBridge handles GET /api/v1/track3/analytics/bridge func (s *Server) HandleBridge(w http.ResponseWriter, r *http.Request) { var chainFrom, chainTo *int if cf := r.URL.Query().Get("chain_from"); cf != "" { if c, err := strconv.Atoi(cf); err == nil { chainFrom = &c } } if ct := r.URL.Query().Get("chain_to"); ct != "" { if c, err := strconv.Atoi(ct); err == nil { chainTo = &c } } var startDate, endDate *time.Time if startStr := r.URL.Query().Get("start_date"); startStr != "" { if t, err := time.Parse(time.RFC3339, startStr); err == nil { startDate = &t } } if endStr := r.URL.Query().Get("end_date"); endStr != "" { if t, err := time.Parse(time.RFC3339, endStr); err == nil { endDate = &t } } stats, err := s.bridgeAnalytics.GetBridgeStats(r.Context(), chainFrom, chainTo, startDate, endDate) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } response := map[string]interface{}{ "data": stats, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleTokenDistribution handles GET /api/v1/track3/analytics/token-distribution func (s *Server) HandleTokenDistribution(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/v1/track3/analytics/token-distribution/") contract := strings.ToLower(path) topN, _ := strconv.Atoi(r.URL.Query().Get("top_n")) if topN < 1 || topN > 1000 { topN = 100 } stats, err := s.tokenDist.GetTokenDistribution(r.Context(), contract, topN) if err != nil { writeError(w, http.StatusNotFound, "not_found", err.Error()) return } response := map[string]interface{}{ "data": stats, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleAddressRisk handles GET /api/v1/track3/analytics/address-risk/:addr func (s *Server) HandleAddressRisk(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/v1/track3/analytics/address-risk/") address := strings.ToLower(path) analysis, err := s.riskAnalyzer.AnalyzeAddress(r.Context(), address) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } response := map[string]interface{}{ "data": analysis, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } 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, }, }) }