170 lines
4.4 KiB
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)
|
|
}
|