package rest import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" ) type addressListRow struct { Address string `json:"address"` TxSent int64 `json:"tx_sent"` TxReceived int64 `json:"tx_received"` TransactionCnt int64 `json:"transaction_count"` TokenCount int64 `json:"token_count"` IsContract bool `json:"is_contract"` Label string `json:"label,omitempty"` LastSeenAt string `json:"last_seen_at,omitempty"` FirstSeenAt string `json:"first_seen_at,omitempty"` } // handleListAddresses handles GET /api/v1/addresses func (s *Server) handleListAddresses(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeMethodNotAllowed(w) return } if !s.requireDB(w) { return } page, pageSize, err := validatePagination( r.URL.Query().Get("page"), r.URL.Query().Get("page_size"), ) if err != nil { writeValidationError(w, err) return } offset := (page - 1) * pageSize search := strings.TrimSpace(r.URL.Query().Get("q")) whereClause := "" args := []interface{}{s.chainID} if search != "" { whereClause = "WHERE LOWER(a.address) LIKE LOWER($2) OR LOWER(COALESCE(l.label, '')) LIKE LOWER($2)" args = append(args, "%"+search+"%") } query := fmt.Sprintf(` WITH activity AS ( SELECT address, COUNT(*) FILTER (WHERE direction = 'sent') AS tx_sent, COUNT(*) FILTER (WHERE direction = 'received') AS tx_received, COUNT(*) AS transaction_count, MIN(seen_at) AS first_seen_at, MAX(seen_at) AS last_seen_at FROM ( SELECT LOWER(t.from_address) AS address, 'sent' AS direction, b.timestamp AS seen_at FROM transactions t JOIN blocks b ON b.chain_id = t.chain_id AND b.number = t.block_number WHERE t.chain_id = $1 AND t.from_address IS NOT NULL AND t.from_address <> '' UNION ALL SELECT LOWER(t.to_address) AS address, 'received' AS direction, b.timestamp AS seen_at FROM transactions t JOIN blocks b ON b.chain_id = t.chain_id AND b.number = t.block_number WHERE t.chain_id = $1 AND t.to_address IS NOT NULL AND t.to_address <> '' ) entries GROUP BY address ), token_activity AS ( SELECT address, COUNT(DISTINCT token_contract) AS token_count FROM ( SELECT LOWER(from_address) AS address, token_contract FROM token_transfers WHERE chain_id = $1 AND from_address IS NOT NULL AND from_address <> '' UNION ALL SELECT LOWER(to_address) AS address, token_contract FROM token_transfers WHERE chain_id = $1 AND to_address IS NOT NULL AND to_address <> '' ) tokens GROUP BY address ), label_activity AS ( SELECT DISTINCT ON (LOWER(address)) LOWER(address) AS address, label FROM address_labels WHERE chain_id = $1 AND label_type = 'public' ORDER BY LOWER(address), updated_at DESC, id DESC ), contract_activity AS ( SELECT LOWER(address) AS address, TRUE AS is_contract FROM contracts WHERE chain_id = $1 ) SELECT a.address, a.tx_sent, a.tx_received, a.transaction_count, COALESCE(t.token_count, 0) AS token_count, COALESCE(c.is_contract, FALSE) AS is_contract, COALESCE(l.label, '') AS label, COALESCE(a.last_seen_at::text, '') AS last_seen_at, COALESCE(a.first_seen_at::text, '') AS first_seen_at FROM activity a LEFT JOIN token_activity t ON t.address = a.address LEFT JOIN label_activity l ON l.address = a.address LEFT JOIN contract_activity c ON c.address = a.address %s ORDER BY a.transaction_count DESC, a.last_seen_at DESC NULLS LAST, a.address ASC LIMIT $%d OFFSET $%d `, whereClause, len(args)+1, len(args)+2) args = append(args, pageSize, offset) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() rows, err := s.db.Query(ctx, query, args...) if err != nil { writeInternalError(w, "Database error") return } defer rows.Close() items := []addressListRow{} for rows.Next() { var row addressListRow if err := rows.Scan( &row.Address, &row.TxSent, &row.TxReceived, &row.TransactionCnt, &row.TokenCount, &row.IsContract, &row.Label, &row.LastSeenAt, &row.FirstSeenAt, ); err != nil { continue } items = append(items, row) } response := map[string]interface{}{ "data": items, "meta": map[string]interface{}{ "pagination": map[string]interface{}{ "page": page, "page_size": pageSize, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }