package track2 import ( "encoding/json" "net/http" "strconv" "strings" "github.com/jackc/pgx/v5/pgxpool" ) // Server handles Track 2 endpoints type Server struct { db *pgxpool.Pool chainID int } // NewServer creates a new Track 2 server func NewServer(db *pgxpool.Pool, chainID int) *Server { return &Server{ db: db, chainID: chainID, } } // HandleAddressTransactions handles GET /api/v1/track2/address/:addr/txs func (s *Server) HandleAddressTransactions(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/track2/address/") parts := strings.Split(path, "/") if len(parts) < 2 || parts[1] != "txs" { writeError(w, http.StatusBadRequest, "bad_request", "Invalid path") return } address := strings.ToLower(parts[0]) page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 || limit > 100 { limit = 20 } offset := (page - 1) * limit query := ` SELECT hash, from_address, to_address, value, block_number, timestamp, status FROM transactions WHERE chain_id = $1 AND (from_address = $2 OR to_address = $2) ORDER BY block_number DESC, timestamp DESC LIMIT $3 OFFSET $4 ` rows, err := s.db.Query(r.Context(), query, s.chainID, address, limit, offset) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } defer rows.Close() transactions := []map[string]interface{}{} for rows.Next() { var hash, from, to, value, status string var blockNumber int64 var timestamp interface{} if err := rows.Scan(&hash, &from, &to, &value, &blockNumber, ×tamp, &status); err != nil { continue } direction := "received" if strings.ToLower(from) == address { direction = "sent" } transactions = append(transactions, map[string]interface{}{ "hash": hash, "from": from, "to": to, "value": value, "block_number": blockNumber, "timestamp": timestamp, "status": status, "direction": direction, }) } // Get total count var total int countQuery := `SELECT COUNT(*) FROM transactions WHERE chain_id = $1 AND (from_address = $2 OR to_address = $2)` s.db.QueryRow(r.Context(), countQuery, s.chainID, address).Scan(&total) response := map[string]interface{}{ "data": transactions, "pagination": map[string]interface{}{ "page": page, "limit": limit, "total": total, "total_pages": (total + limit - 1) / limit, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleAddressTokens handles GET /api/v1/track2/address/:addr/tokens func (s *Server) HandleAddressTokens(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/track2/address/") parts := strings.Split(path, "/") if len(parts) < 2 || parts[1] != "tokens" { writeError(w, http.StatusBadRequest, "bad_request", "Invalid path") return } address := strings.ToLower(parts[0]) query := ` SELECT token_contract, balance, last_updated_timestamp FROM token_balances WHERE address = $1 AND chain_id = $2 AND balance > 0 ORDER BY balance DESC ` rows, err := s.db.Query(r.Context(), query, address, s.chainID) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } defer rows.Close() tokens := []map[string]interface{}{} for rows.Next() { var contract, balance string var lastUpdated interface{} if err := rows.Scan(&contract, &balance, &lastUpdated); err != nil { continue } tokens = append(tokens, map[string]interface{}{ "contract": contract, "balance": balance, "balance_formatted": balance, // TODO: Format with decimals "last_updated": lastUpdated, }) } response := map[string]interface{}{ "data": map[string]interface{}{ "address": address, "tokens": tokens, "total_tokens": len(tokens), }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleTokenInfo handles GET /api/v1/track2/token/:contract func (s *Server) HandleTokenInfo(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/track2/token/") contract := strings.ToLower(path) // Get token info from token_transfers query := ` SELECT COUNT(DISTINCT from_address) + COUNT(DISTINCT to_address) as holders, COUNT(*) as transfers_24h, SUM(value) as volume_24h FROM token_transfers WHERE token_contract = $1 AND chain_id = $2 AND timestamp >= NOW() - INTERVAL '24 hours' ` var holders, transfers24h int var volume24h string err := s.db.QueryRow(r.Context(), query, contract, s.chainID).Scan(&holders, &transfers24h, &volume24h) if err != nil { writeError(w, http.StatusNotFound, "not_found", "Token not found") return } response := map[string]interface{}{ "data": map[string]interface{}{ "contract": contract, "holders": holders, "transfers_24h": transfers24h, "volume_24h": volume24h, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleSearch handles GET /api/v1/track2/search?q= func (s *Server) HandleSearch(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } query := r.URL.Query().Get("q") if query == "" { writeError(w, http.StatusBadRequest, "bad_request", "Query parameter 'q' is required") return } query = strings.ToLower(strings.TrimPrefix(query, "0x")) // Try to detect type and search var result map[string]interface{} // Check if it's a block number if blockNum, err := strconv.ParseInt(query, 10, 64); err == nil { var hash string err := s.db.QueryRow(r.Context(), `SELECT hash FROM blocks WHERE chain_id = $1 AND number = $2`, s.chainID, blockNum).Scan(&hash) if err == nil { result = map[string]interface{}{ "type": "block", "result": map[string]interface{}{ "number": blockNum, "hash": hash, }, } } } else if len(query) == 64 || len(query) == 40 { // Could be address or transaction hash fullQuery := "0x" + query // Check transaction var txHash string err := s.db.QueryRow(r.Context(), `SELECT hash FROM transactions WHERE chain_id = $1 AND hash = $2`, s.chainID, fullQuery).Scan(&txHash) if err == nil { result = map[string]interface{}{ "type": "transaction", "result": map[string]interface{}{ "hash": txHash, }, } } else { // Check address var balance string err := s.db.QueryRow(r.Context(), `SELECT COALESCE(SUM(balance), '0') FROM token_balances WHERE address = $1 AND chain_id = $2`, fullQuery, s.chainID).Scan(&balance) if err == nil { result = map[string]interface{}{ "type": "address", "result": map[string]interface{}{ "address": fullQuery, "balance": balance, }, } } } } if result == nil { writeError(w, http.StatusNotFound, "not_found", "No results found") return } response := map[string]interface{}{ "data": result, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // HandleInternalTransactions handles GET /api/v1/track2/address/:addr/internal-txs func (s *Server) HandleInternalTransactions(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/track2/address/") parts := strings.Split(path, "/") if len(parts) < 2 || parts[1] != "internal-txs" { writeError(w, http.StatusBadRequest, "bad_request", "Invalid path") return } address := strings.ToLower(parts[0]) page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 || limit > 100 { limit = 20 } offset := (page - 1) * limit query := ` SELECT transaction_hash, from_address, to_address, value, block_number, timestamp FROM internal_transactions WHERE chain_id = $1 AND (from_address = $2 OR to_address = $2) ORDER BY block_number DESC, timestamp DESC LIMIT $3 OFFSET $4 ` rows, err := s.db.Query(r.Context(), query, s.chainID, address, limit, offset) if err != nil { writeError(w, http.StatusInternalServerError, "database_error", err.Error()) return } defer rows.Close() internalTxs := []map[string]interface{}{} for rows.Next() { var txHash, from, to, value string var blockNumber int64 var timestamp interface{} if err := rows.Scan(&txHash, &from, &to, &value, &blockNumber, ×tamp); err != nil { continue } internalTxs = append(internalTxs, map[string]interface{}{ "transaction_hash": txHash, "from": from, "to": to, "value": value, "block_number": blockNumber, "timestamp": timestamp, }) } var total int countQuery := `SELECT COUNT(*) FROM internal_transactions WHERE chain_id = $1 AND (from_address = $2 OR to_address = $2)` s.db.QueryRow(r.Context(), countQuery, s.chainID, address).Scan(&total) response := map[string]interface{}{ "data": internalTxs, "pagination": map[string]interface{}{ "page": page, "limit": limit, "total": total, "total_pages": (total + limit - 1) / limit, }, } 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, }, }) }