Add explorer liquidity access and live route proxies

This commit is contained in:
defiQUG
2026-03-27 12:02:36 -07:00
parent d02ee71cf6
commit 2491336b8e
17 changed files with 2746 additions and 125 deletions

View 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)
}

View File

@@ -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)

View 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 ""
}