Files
explorer-monorepo/backend/api/rest/addresses_list.go

170 lines
4.4 KiB
Go

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
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
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_address) AS token_count
FROM (
SELECT from_address AS address, token_address
FROM token_transfers
WHERE chain_id = $1 AND from_address IS NOT NULL AND from_address <> ''
UNION ALL
SELECT to_address AS address, token_address
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 (address)
address,
label
FROM address_labels
WHERE chain_id = $1 AND label_type = 'public'
ORDER BY address, updated_at DESC, id DESC
),
contract_activity AS (
SELECT 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)
}