Files
zeekayandHanzo Dev 64519916bf the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:01:04 -07:00

1099 lines
32 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package api
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/luxfi/indexer/evm/account"
)
// Config for the API server
type Config struct {
HTTPPort int
ChainID int64
ChainName string
ChainSlug string // table prefix, e.g. "cchain", "zoo", "hanzo"
DatabaseURL string
RPCEndpoint string
}
// Server provides REST, RPC, GraphQL and WebSocket APIs
type Server struct {
config Config
db *sql.DB
repo *Repository
rpcHandler *RPCHandler
wsHub *WebSocketHub
router *mux.Router
iam *IAMMiddleware
accountAPI *AccountAPI
accountSvc *account.Service
}
// NewServer creates a new API server
func NewServer(cfg Config, db *sql.DB) *Server {
repo := NewRepository(db, cfg.ChainID, cfg.ChainSlug)
wsHub := NewWebSocketHub()
// Initialize account service
acctSvc := account.NewService(db, account.DefaultConfig())
// Initialize IAM middleware
iamCfg := IAMConfigFromEnv()
iamMiddleware := NewIAMMiddleware(iamCfg, acctSvc)
if iamCfg.Enabled {
log.Printf("[IAM] authentication enabled (server=%s, client=%s)", iamCfg.ServerURL, iamCfg.ClientID)
} else {
log.Printf("[IAM] authentication disabled (set IAM_ENABLED=true to enable)")
}
s := &Server{
config: cfg,
db: db,
repo: repo,
rpcHandler: NewRPCHandler(repo),
wsHub: wsHub,
router: mux.NewRouter(),
iam: iamMiddleware,
accountAPI: NewAccountAPI(acctSvc),
accountSvc: acctSvc,
}
s.setupRoutes()
return s
}
func (s *Server) setupRoutes() {
// Health check
s.router.HandleFunc("/health", s.handleHealth).Methods("GET")
s.router.HandleFunc("/api/healthcheck", s.handleHealth).Methods("GET")
// REST API — explorer v2 endpoints
api := s.router.PathPrefix("/v1/explorer").Subrouter()
// Blocks
api.HandleFunc("/blocks", s.handleBlocks).Methods("GET")
api.HandleFunc("/blocks/{block_hash_or_number}", s.handleBlock).Methods("GET")
api.HandleFunc("/blocks/{block_hash_or_number}/transactions", s.handleBlockTransactions).Methods("GET")
// Transactions
api.HandleFunc("/transactions", s.handleTransactions).Methods("GET")
api.HandleFunc("/transactions/{transaction_hash}", s.handleTransaction).Methods("GET")
api.HandleFunc("/transactions/{transaction_hash}/token-transfers", s.handleTxTokenTransfers).Methods("GET")
api.HandleFunc("/transactions/{transaction_hash}/internal-transactions", s.handleTxInternalTxs).Methods("GET")
api.HandleFunc("/transactions/{transaction_hash}/logs", s.handleTxLogs).Methods("GET")
api.HandleFunc("/transactions/{transaction_hash}/raw-trace", s.handleTxRawTrace).Methods("GET")
api.HandleFunc("/transactions/{transaction_hash}/state-changes", s.handleTxStateChanges).Methods("GET")
// Addresses
api.HandleFunc("/addresses", s.handleAddresses).Methods("GET")
api.HandleFunc("/addresses/{address_hash}", s.handleAddress).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/transactions", s.handleAddressTransactions).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/token-transfers", s.handleAddressTokenTransfers).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/internal-transactions", s.handleAddressInternalTxs).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/logs", s.handleAddressLogs).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/tokens", s.handleAddressTokens).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/coin-balance-history", s.handleAddressBalanceHistory).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/coin-balance-history-by-day", s.handleAddressBalanceByDay).Methods("GET")
api.HandleFunc("/addresses/{address_hash}/counters", s.handleAddressCounters).Methods("GET")
// Tokens
api.HandleFunc("/tokens", s.handleTokens).Methods("GET")
api.HandleFunc("/tokens/{address_hash}", s.handleToken).Methods("GET")
api.HandleFunc("/tokens/{address_hash}/transfers", s.handleTokenTransfers).Methods("GET")
api.HandleFunc("/tokens/{address_hash}/holders", s.handleTokenHolders).Methods("GET")
api.HandleFunc("/tokens/{address_hash}/instances", s.handleTokenInstances).Methods("GET")
api.HandleFunc("/tokens/{address_hash}/instances/{token_id}", s.handleTokenInstance).Methods("GET")
// Smart Contracts
api.HandleFunc("/smart-contracts", s.handleSmartContracts).Methods("GET")
api.HandleFunc("/smart-contracts/{address_hash}", s.handleSmartContract).Methods("GET")
api.HandleFunc("/smart-contracts/{address_hash}/methods-read", s.handleContractReadMethods).Methods("GET")
api.HandleFunc("/smart-contracts/{address_hash}/methods-write", s.handleContractWriteMethods).Methods("GET")
api.HandleFunc("/smart-contracts/{address_hash}/query-read-method", s.handleContractQuery).Methods("POST")
// Search
api.HandleFunc("/search", s.handleSearch).Methods("GET")
api.HandleFunc("/search/check-redirect", s.handleSearchRedirect).Methods("GET")
// Stats
api.HandleFunc("/stats", s.handleStats).Methods("GET")
api.HandleFunc("/stats/charts/transactions", s.handleTxChart).Methods("GET")
api.HandleFunc("/stats/charts/market", s.handleMarketChart).Methods("GET")
// Homepage
api.HandleFunc("/homepage/blocks", s.handleMainPageBlocks).Methods("GET")
api.HandleFunc("/homepage/transactions", s.handleMainPageTransactions).Methods("GET")
api.HandleFunc("/homepage/indexing-status", s.handleIndexingStatus).Methods("GET")
// WebSocket
api.HandleFunc("/blocks/subscribe", s.handleBlocksSubscribe)
api.HandleFunc("/transactions/subscribe", s.handleTransactionsSubscribe)
api.HandleFunc("/addresses/{address_hash}/subscribe", s.handleAddressSubscribe)
// Etherscan-compatible RPC API
s.router.HandleFunc("/api", s.handleRPC).Methods("GET", "POST")
// GraphQL
api.HandleFunc("/graphql", s.handleGraphQL).Methods("POST", "GET")
// Account API (requires IAM authentication)
acct := s.router.PathPrefix("/api/account/v2").Subrouter()
acct.Use(s.iam.RequireAuth)
acct.HandleFunc("/get_csrf", s.accountAPI.HandleGetCSRF).Methods("GET")
acct.HandleFunc("/user/info", s.accountAPI.HandleGetUserInfo).Methods("GET")
acct.HandleFunc("/user/api_keys", s.accountAPI.HandleListAPIKeys).Methods("GET")
acct.HandleFunc("/user/api_keys", s.accountAPI.HandleCreateAPIKey).Methods("POST")
acct.HandleFunc("/user/api_keys/{id}", s.accountAPI.HandleDeleteAPIKey).Methods("DELETE")
acct.HandleFunc("/user/watchlist", s.accountAPI.HandleListWatchlists).Methods("GET")
acct.HandleFunc("/user/watchlist/{id}", s.accountAPI.HandleGetWatchlistAddresses).Methods("GET")
acct.HandleFunc("/user/watchlist/{id}", s.accountAPI.HandleAddWatchlistAddress).Methods("POST")
acct.HandleFunc("/user/watchlist/{id}/{address_hash}", s.accountAPI.HandleRemoveWatchlistAddress).Methods("DELETE")
acct.HandleFunc("/user/tags/address", s.accountAPI.HandleListAddressTags).Methods("GET")
acct.HandleFunc("/user/tags/address", s.accountAPI.HandleSetAddressTag).Methods("POST")
acct.HandleFunc("/user/tags/address/{id}", s.accountAPI.HandleDeleteAddressTag).Methods("DELETE")
acct.HandleFunc("/user/custom_abis", s.accountAPI.HandleListCustomABIs).Methods("GET")
acct.HandleFunc("/user/custom_abis", s.accountAPI.HandleSetCustomABI).Methods("POST")
acct.HandleFunc("/user/custom_abis/{id}", s.accountAPI.HandleDeleteCustomABI).Methods("DELETE")
s.router.HandleFunc("/api/account/auth/logout", s.accountAPI.HandleLogout).Methods("GET", "POST")
}
// CORS middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Content-Type", "application/json")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Run starts the API server
func (s *Server) Run(ctx context.Context) error {
go s.wsHub.Run(ctx)
handler := corsMiddleware(s.router)
server := &http.Server{
Addr: fmt.Sprintf(":%d", s.config.HTTPPort),
Handler: handler,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
}()
log.Printf("[API] Server starting on port %d", s.config.HTTPPort)
log.Printf("[API] REST API: http://localhost:%d/v1/explorer/", s.config.HTTPPort)
log.Printf("[API] Etherscan RPC: http://localhost:%d/api", s.config.HTTPPort)
log.Printf("[API] GraphQL: http://localhost:%d/v1/explorer/graphql", s.config.HTTPPort)
return server.ListenAndServe()
}
// Router returns the HTTP router for testing
func (s *Server) Router() *mux.Router {
return s.router
}
// BroadcastBlock sends a new block to all subscribers
func (s *Server) BroadcastBlock(block *Block) {
s.wsHub.BroadcastBlock(block)
}
// BroadcastTransaction sends a new transaction to subscribers
func (s *Server) BroadcastTransaction(tx *Transaction) {
s.wsHub.BroadcastTransaction(tx)
}
// Helper functions
func (s *Server) writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func (s *Server) writeError(w http.ResponseWriter, status int, message string) {
s.writeJSON(w, status, ErrorResponse{Message: message})
}
func getPagination(r *http.Request) (page, pageSize int) {
page = 0
pageSize = 50
if p := r.URL.Query().Get("page"); p != "" {
if n, err := strconv.Atoi(p); err == nil && n >= 0 {
page = n
}
}
if ps := r.URL.Query().Get("page_size"); ps != "" {
if n, err := strconv.Atoi(ps); err == nil && n > 0 && n <= 100 {
pageSize = n
}
}
// Also support items_count for cursor-based pagination
if ic := r.URL.Query().Get("items_count"); ic != "" {
if n, err := strconv.Atoi(ic); err == nil && n > 0 && n <= 100 {
pageSize = n
}
}
return
}
func parseBlockParam(param string) (uint64, bool, error) {
// Check if it's a hash (0x...)
if strings.HasPrefix(param, "0x") && len(param) == 66 {
return 0, true, nil
}
// Try to parse as number
n, err := strconv.ParseUint(param, 10, 64)
if err != nil {
return 0, false, fmt.Errorf("invalid block parameter")
}
return n, false, nil
}
// Handler implementations
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "ok",
"chain_id": s.config.ChainID,
"chain_name": s.config.ChainName,
"version": "1.0.0",
})
}
func (s *Server) handleBlocks(w http.ResponseWriter, r *http.Request) {
page, pageSize := getPagination(r)
blockType := r.URL.Query().Get("type")
result, err := s.repo.GetBlocks(r.Context(), page, pageSize, &BlockFilters{Type: blockType})
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleBlock(w http.ResponseWriter, r *http.Request) {
param := mux.Vars(r)["block_hash_or_number"]
height, isHash, err := parseBlockParam(param)
if err != nil {
s.writeError(w, http.StatusBadRequest, err.Error())
return
}
var block *Block
if isHash {
block, err = s.repo.GetBlockByHash(r.Context(), param)
} else {
block, err = s.repo.GetBlockByNumber(r.Context(), height)
}
if err == ErrNotFound {
s.writeError(w, http.StatusNotFound, "block not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, block)
}
func (s *Server) handleBlockTransactions(w http.ResponseWriter, r *http.Request) {
param := mux.Vars(r)["block_hash_or_number"]
page, pageSize := getPagination(r)
height, isHash, err := parseBlockParam(param)
if err != nil {
s.writeError(w, http.StatusBadRequest, err.Error())
return
}
if isHash {
block, err := s.repo.GetBlockByHash(r.Context(), param)
if err != nil {
s.writeError(w, http.StatusNotFound, "block not found")
return
}
height = block.Height
}
result, err := s.repo.GetTransactionsByBlock(r.Context(), height, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleTransactions(w http.ResponseWriter, r *http.Request) {
page, pageSize := getPagination(r)
result, err := s.repo.GetTransactions(r.Context(), page, pageSize, nil)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleTransaction(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["transaction_hash"]
tx, err := s.repo.GetTransactionByHash(r.Context(), hash)
if err == ErrNotFound {
s.writeError(w, http.StatusNotFound, "transaction not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, tx)
}
func (s *Server) handleTxTokenTransfers(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["transaction_hash"]
page, pageSize := getPagination(r)
// Query token transfers for this transaction
rows, err := s.db.QueryContext(r.Context(), fmt.Sprintf(`
SELECT t.id, t.tx_hash, t.log_index, t.block_number, t.token_address,
t.token_type, t.tx_from, t.tx_to, t.value, t.token_id, t.timestamp,
COALESCE(tk.name, ''), COALESCE(tk.symbol, ''), COALESCE(tk.decimals, 18)
FROM %s t
LEFT JOIN %s tk ON t.token_address = tk.address
WHERE t.tx_hash = $1
ORDER BY t.log_index ASC
LIMIT $2 OFFSET $3
`, s.repo.Tbl("token_transfers"), s.repo.Tbl("tokens")), strings.ToLower(hash), pageSize+1, page*pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
var transfers []TokenTransfer
for rows.Next() {
var id, txHash, tokenAddr, tokenType, from, to, value string
var tokenID sql.NullString
var blockNumber uint64
var logIndex int
var timestamp time.Time
var tokenName, tokenSymbol string
var tokenDecimals uint8
if err := rows.Scan(&id, &txHash, &logIndex, &blockNumber, &tokenAddr, &tokenType,
&from, &to, &value, &tokenID, &timestamp, &tokenName, &tokenSymbol, &tokenDecimals); err != nil {
continue
}
transfer := TokenTransfer{
TxHash: txHash,
BlockNumber: blockNumber,
LogIndex: logIndex,
Timestamp: &timestamp,
From: &Address{Hash: from},
To: &Address{Hash: to},
Token: &Token{
Address: tokenAddr,
Name: tokenName,
Symbol: tokenSymbol,
Decimals: &tokenDecimals,
Type: tokenType,
},
Total: &TokenTotal{Value: value, Decimals: &tokenDecimals},
}
if tokenID.Valid {
transfer.TokenID = tokenID.String
}
transfers = append(transfers, transfer)
}
resp := &PaginatedResponse{Items: transfers}
if len(transfers) > pageSize {
transfers = transfers[:pageSize]
resp.Items = transfers
}
s.writeJSON(w, http.StatusOK, resp)
}
func (s *Server) handleTxInternalTxs(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["transaction_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetInternalTransactions(r.Context(), hash, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleTxLogs(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["transaction_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetLogs(r.Context(), "", 0, 0, nil, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Filter by tx hash
if items, ok := result.Items.([]Log); ok {
var filtered []Log
for _, log := range items {
if strings.EqualFold(log.TxHash, hash) {
filtered = append(filtered, log)
}
}
result.Items = filtered
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleTxRawTrace(w http.ResponseWriter, r *http.Request) {
// Return empty trace for now
s.writeJSON(w, http.StatusOK, map[string]interface{}{})
}
func (s *Server) handleTxStateChanges(w http.ResponseWriter, r *http.Request) {
// Return empty state changes for now
s.writeJSON(w, http.StatusOK, &PaginatedResponse{Items: []interface{}{}})
}
func (s *Server) handleAddresses(w http.ResponseWriter, r *http.Request) {
// List addresses with balances
page, pageSize := getPagination(r)
rows, err := s.db.QueryContext(r.Context(), fmt.Sprintf(`
SELECT hash, balance, tx_count, is_contract
FROM %s
ORDER BY CAST(balance AS NUMERIC) DESC
LIMIT $1 OFFSET $2
`, s.repo.Tbl("addresses")), pageSize+1, page*pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
var addresses []Address
for rows.Next() {
var hash, balance string
var txCount uint64
var isContract bool
if rows.Scan(&hash, &balance, &txCount, &isContract) == nil {
addresses = append(addresses, Address{
Hash: hash,
Balance: balance,
TransactionCount: &txCount,
IsContract: isContract,
})
}
}
resp := &PaginatedResponse{Items: addresses}
if len(addresses) > pageSize {
addresses = addresses[:pageSize]
resp.Items = addresses
}
s.writeJSON(w, http.StatusOK, resp)
}
func (s *Server) handleAddress(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
addr, err := s.repo.GetAddress(r.Context(), hash)
if err == ErrNotFound {
s.writeError(w, http.StatusNotFound, "address not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, addr)
}
func (s *Server) handleAddressTransactions(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetAddressTransactions(r.Context(), hash, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleAddressTokenTransfers(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetAddressTokenTransfers(r.Context(), hash, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleAddressInternalTxs(w http.ResponseWriter, r *http.Request) {
// Internal transactions for an address
s.writeJSON(w, http.StatusOK, &PaginatedResponse{Items: []interface{}{}})
}
func (s *Server) handleAddressLogs(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetLogs(r.Context(), hash, 0, 0, nil, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleAddressTokens(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
hash = strings.ToLower(hash)
page, pageSize := getPagination(r)
rows, err := s.db.QueryContext(r.Context(), fmt.Sprintf(`
SELECT tb.token_address, tb.balance, tb.token_id,
t.name, t.symbol, t.decimals, t.token_type
FROM %s tb
LEFT JOIN %s t ON tb.token_address = t.address
WHERE tb.holder_address = $1 AND CAST(tb.balance AS NUMERIC) > 0
ORDER BY CAST(tb.balance AS NUMERIC) DESC
LIMIT $2 OFFSET $3
`, s.repo.Tbl("token_balances"), s.repo.Tbl("tokens")), hash, pageSize+1, page*pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
var balances []TokenBalance
for rows.Next() {
var tokenAddr, balance string
var tokenID sql.NullString
var name, symbol, tokenType sql.NullString
var decimals sql.NullInt16
if rows.Scan(&tokenAddr, &balance, &tokenID, &name, &symbol, &decimals, &tokenType) == nil {
var d uint8 = 18
if decimals.Valid {
d = uint8(decimals.Int16)
}
tb := TokenBalance{
Token: &Token{
Address: tokenAddr,
Name: name.String,
Symbol: symbol.String,
Decimals: &d,
Type: tokenType.String,
},
Value: balance,
}
if tokenID.Valid {
tb.TokenID = tokenID.String
}
balances = append(balances, tb)
}
}
resp := &PaginatedResponse{Items: balances}
if len(balances) > pageSize {
balances = balances[:pageSize]
resp.Items = balances
}
s.writeJSON(w, http.StatusOK, resp)
}
func (s *Server) handleAddressBalanceHistory(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetAddressCoinBalanceHistory(r.Context(), hash, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleAddressBalanceByDay(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
page, pageSize := getPagination(r)
result, err := s.repo.GetAddressCoinBalanceByDay(r.Context(), hash, page, pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleAddressCounters(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
hash = strings.ToLower(hash)
var txCount, tokenTransferCount int64
s.db.QueryRowContext(r.Context(), fmt.Sprintf(`
SELECT COUNT(*) FROM %s WHERE tx_from = $1 OR tx_to = $1
`, s.repo.Tbl("transactions")), hash).Scan(&txCount)
s.db.QueryRowContext(r.Context(), fmt.Sprintf(`
SELECT COUNT(*) FROM %s WHERE tx_from = $1 OR tx_to = $1
`, s.repo.Tbl("token_transfers")), hash).Scan(&tokenTransferCount)
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"transactions_count": txCount,
"token_transfers_count": tokenTransferCount,
"validations_count": 0,
"gas_usage_count": 0,
})
}
func (s *Server) handleTokens(w http.ResponseWriter, r *http.Request) {
page, pageSize := getPagination(r)
tokenType := r.URL.Query().Get("type")
result, err := s.repo.GetTokens(r.Context(), page, pageSize, tokenType)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result)
}
func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
token, err := s.repo.GetToken(r.Context(), hash)
if err == ErrNotFound {
s.writeError(w, http.StatusNotFound, "token not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, token)
}
func (s *Server) handleTokenTransfers(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
hash = strings.ToLower(hash)
page, pageSize := getPagination(r)
rows, err := s.db.QueryContext(r.Context(), fmt.Sprintf(`
SELECT id, tx_hash, log_index, block_number, tx_from, tx_to, value, token_id, timestamp
FROM %s
WHERE token_address = $1
ORDER BY block_number DESC, log_index DESC
LIMIT $2 OFFSET $3
`, s.repo.Tbl("token_transfers")), hash, pageSize+1, page*pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
var transfers []TokenTransfer
for rows.Next() {
var id, txHash, from, to, value string
var tokenID sql.NullString
var blockNumber uint64
var logIndex int
var timestamp time.Time
if rows.Scan(&id, &txHash, &logIndex, &blockNumber, &from, &to, &value, &tokenID, &timestamp) == nil {
transfer := TokenTransfer{
TxHash: txHash,
BlockNumber: blockNumber,
LogIndex: logIndex,
Timestamp: &timestamp,
From: &Address{Hash: from},
To: &Address{Hash: to},
Total: &TokenTotal{Value: value},
}
if tokenID.Valid {
transfer.TokenID = tokenID.String
}
transfers = append(transfers, transfer)
}
}
resp := &PaginatedResponse{Items: transfers}
if len(transfers) > pageSize {
transfers = transfers[:pageSize]
resp.Items = transfers
}
s.writeJSON(w, http.StatusOK, resp)
}
func (s *Server) handleTokenHolders(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
hash = strings.ToLower(hash)
page, pageSize := getPagination(r)
rows, err := s.db.QueryContext(r.Context(), fmt.Sprintf(`
SELECT holder_address, balance
FROM %s
WHERE token_address = $1 AND CAST(balance AS NUMERIC) > 0
ORDER BY CAST(balance AS NUMERIC) DESC
LIMIT $2 OFFSET $3
`, s.repo.Tbl("token_balances")), hash, pageSize+1, page*pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
type Holder struct {
Address *Address `json:"address"`
Value string `json:"value"`
}
var holders []Holder
for rows.Next() {
var address, balance string
if rows.Scan(&address, &balance) == nil {
holders = append(holders, Holder{
Address: &Address{Hash: address},
Value: balance,
})
}
}
resp := &PaginatedResponse{Items: holders}
if len(holders) > pageSize {
holders = holders[:pageSize]
resp.Items = holders
}
s.writeJSON(w, http.StatusOK, resp)
}
func (s *Server) handleTokenInstances(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, &PaginatedResponse{Items: []interface{}{}})
}
func (s *Server) handleTokenInstance(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{})
}
func (s *Server) handleSmartContracts(w http.ResponseWriter, r *http.Request) {
page, pageSize := getPagination(r)
rows, err := s.db.QueryContext(r.Context(), fmt.Sprintf(`
SELECT hash, tx_count FROM %s
WHERE is_contract = TRUE
ORDER BY tx_count DESC
LIMIT $1 OFFSET $2
`, s.repo.Tbl("addresses")), pageSize+1, page*pageSize)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
var contracts []SmartContract
for rows.Next() {
var address string
var txCount int64
if rows.Scan(&address, &txCount) == nil {
contracts = append(contracts, SmartContract{
Address: address,
})
}
}
resp := &PaginatedResponse{Items: contracts}
if len(contracts) > pageSize {
contracts = contracts[:pageSize]
resp.Items = contracts
}
s.writeJSON(w, http.StatusOK, resp)
}
func (s *Server) handleSmartContract(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["address_hash"]
contract, err := s.repo.GetSmartContract(r.Context(), hash)
if err == ErrNotFound {
s.writeError(w, http.StatusNotFound, "contract not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, contract)
}
func (s *Server) handleContractReadMethods(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, []interface{}{})
}
func (s *Server) handleContractWriteMethods(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, []interface{}{})
}
func (s *Server) handleContractQuery(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{"result": nil})
}
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
s.writeJSON(w, http.StatusOK, &PaginatedResponse{Items: []interface{}{}})
return
}
results, err := s.repo.Search(r.Context(), query, 50)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, &PaginatedResponse{Items: results})
}
func (s *Server) handleSearchRedirect(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
s.writeJSON(w, http.StatusOK, map[string]interface{}{"redirect": false})
return
}
results, _ := s.repo.Search(r.Context(), query, 1)
if len(results) == 1 {
result := results[0]
var redirectURL string
switch result.Type {
case "block":
redirectURL = fmt.Sprintf("/block/%d", *result.BlockNumber)
case "transaction":
redirectURL = fmt.Sprintf("/tx/%s", result.Hash)
case "address", "contract":
redirectURL = fmt.Sprintf("/address/%s", result.Address)
case "token":
redirectURL = fmt.Sprintf("/token/%s", result.Address)
}
if redirectURL != "" {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"redirect": true,
"type": result.Type,
"redirect_to": redirectURL,
})
return
}
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"redirect": false})
}
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
stats, err := s.repo.GetStats(r.Context())
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, stats)
}
func (s *Server) handleTxChart(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"chart_data": []interface{}{},
"available_supply": "0",
})
}
func (s *Server) handleMarketChart(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"chart_data": []interface{}{},
"available_supply": "0",
})
}
func (s *Server) handleMainPageBlocks(w http.ResponseWriter, r *http.Request) {
result, err := s.repo.GetBlocks(r.Context(), 0, 6, nil)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result.Items)
}
func (s *Server) handleMainPageTransactions(w http.ResponseWriter, r *http.Request) {
result, err := s.repo.GetTransactions(r.Context(), 0, 6, nil)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, result.Items)
}
func (s *Server) handleIndexingStatus(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"finished_indexing": true,
"finished_indexing_blocks": true,
"indexed_blocks_ratio": "1.00",
"indexed_internal_transactions_ratio": "1.00",
})
}
// WebSocket handlers
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func (s *Server) handleBlocksSubscribe(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
s.wsHub.RegisterBlockClient(conn)
}
func (s *Server) handleTransactionsSubscribe(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
s.wsHub.RegisterTransactionClient(conn)
}
func (s *Server) handleAddressSubscribe(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
address := strings.ToLower(mux.Vars(r)["address_hash"])
s.wsHub.RegisterAddressClient(conn, address)
}
// RPC handler (Etherscan compatible)
func (s *Server) handleRPC(w http.ResponseWriter, r *http.Request) {
s.rpcHandler.Handle(w, r)
}
// GraphQL handler
func (s *Server) handleGraphQL(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
// Return GraphQL playground
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html>
<html>
<head>
<title>GraphQL Playground</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
</head>
<body>
<div id="root"></div>
<script>
window.addEventListener('load', function() {
GraphQLPlayground.init(document.getElementById('root'), { endpoint: '/v1/explorer/graphql' })
})
</script>
</body>
</html>`))
return
}
// Parse GraphQL query
var req struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request")
return
}
// Simple query handling - full implementation would use gqlgen
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"data": map[string]interface{}{
"__typename": "Query",
},
})
}