Add explorer liquidity access and live route proxies
This commit is contained in:
169
backend/api/rest/addresses_list.go
Normal file
169
backend/api/rest/addresses_list.go
Normal file
@@ -0,0 +1,169 @@
|
||||
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)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/transactions/", s.handleTransactionDetail)
|
||||
|
||||
// Address routes
|
||||
mux.HandleFunc("/api/v1/addresses", s.handleListAddresses)
|
||||
mux.HandleFunc("/api/v1/addresses/", s.handleAddressDetail)
|
||||
|
||||
// Search route
|
||||
@@ -38,6 +39,10 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) {
|
||||
// Feature flags endpoint
|
||||
mux.HandleFunc("/api/v1/features", s.handleFeatures)
|
||||
|
||||
// Route decision tree proxy
|
||||
mux.HandleFunc("/api/v1/routes/tree", s.handleRouteDecisionTree)
|
||||
mux.HandleFunc("/api/v1/routes/depth", s.handleRouteDepth)
|
||||
|
||||
// Auth endpoints
|
||||
mux.HandleFunc("/api/v1/auth/nonce", s.handleAuthNonce)
|
||||
mux.HandleFunc("/api/v1/auth/wallet", s.handleAuthWallet)
|
||||
|
||||
57
backend/api/rest/routes_proxy.go
Normal file
57
backend/api/rest/routes_proxy.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Server) handleRouteDecisionTree(w http.ResponseWriter, r *http.Request) {
|
||||
s.proxyRouteTreeEndpoint(w, r, "/api/v1/routes/tree")
|
||||
}
|
||||
|
||||
func (s *Server) handleRouteDepth(w http.ResponseWriter, r *http.Request) {
|
||||
s.proxyRouteTreeEndpoint(w, r, "/api/v1/routes/depth")
|
||||
}
|
||||
|
||||
func (s *Server) proxyRouteTreeEndpoint(w http.ResponseWriter, r *http.Request, path string) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
baseURL := strings.TrimSpace(firstNonEmptyEnv(
|
||||
"TOKEN_AGGREGATION_API_BASE",
|
||||
"TOKEN_AGGREGATION_URL",
|
||||
"TOKEN_AGGREGATION_BASE_URL",
|
||||
))
|
||||
if baseURL == "" {
|
||||
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "token aggregation api base url is not configured")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := url.Parse(strings.TrimRight(baseURL, "/"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, "bad_gateway", fmt.Sprintf("invalid token aggregation api base url: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, proxyErr error) {
|
||||
writeError(rw, http.StatusBadGateway, "bad_gateway", fmt.Sprintf("route tree proxy failed for %s: %v", path, proxyErr))
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func firstNonEmptyEnv(keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user