mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
791 lines
23 KiB
Go
791 lines
23 KiB
Go
package explorer
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Paginated response for explorer API v2.
|
|
type paginatedResponse struct {
|
|
Items any `json:"items"`
|
|
NextPageParams any `json:"next_page_params"`
|
|
}
|
|
|
|
func emptyPage() paginatedResponse {
|
|
return paginatedResponse{Items: []any{}, NextPageParams: nil}
|
|
}
|
|
|
|
// ---- Blocks ----
|
|
|
|
func (s *Service) handleListBlocks(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
q := r.URL.Query()
|
|
limit := intParam(q.Get("items_count"), 50)
|
|
|
|
query := "SELECT * FROM blocks WHERE chain_id = ? ORDER BY number DESC LIMIT ?"
|
|
args := []any{s.config.ChainID, limit + 1}
|
|
|
|
if bn := q.Get("block_number"); bn != "" {
|
|
query = "SELECT * FROM blocks WHERE chain_id = ? AND number < ? ORDER BY number DESC LIMIT ?"
|
|
args = []any{s.config.ChainID, bn, limit + 1}
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
blocks, err := scanMaps(rows)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
|
|
var nextPage any
|
|
if len(blocks) > limit {
|
|
last := blocks[limit-1]
|
|
nextPage = map[string]any{
|
|
"block_number": last["number"],
|
|
"items_count": limit,
|
|
}
|
|
blocks = blocks[:limit]
|
|
}
|
|
|
|
items := make([]map[string]any, len(blocks))
|
|
for i, b := range blocks {
|
|
items[i] = formatBlock(b)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nextPage})
|
|
}
|
|
|
|
func (s *Service) handleGetBlock(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
id := chi.URLParam(r, "block_hash_or_number")
|
|
|
|
var query string
|
|
var args []any
|
|
if strings.HasPrefix(id, "0x") {
|
|
query = "SELECT * FROM blocks WHERE chain_id = ? AND hash = ? LIMIT 1"
|
|
args = []any{s.config.ChainID, hexToBytes(id)}
|
|
} else {
|
|
query = "SELECT * FROM blocks WHERE chain_id = ? AND number = ? LIMIT 1"
|
|
args = []any{s.config.ChainID, id}
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
notFoundError(w, "block not found")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
blocks, err := scanMaps(rows)
|
|
if err != nil || len(blocks) == 0 {
|
|
notFoundError(w, "block not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, formatBlock(blocks[0]))
|
|
}
|
|
|
|
func (s *Service) handleBlockTransactions(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
id := chi.URLParam(r, "block_hash_or_number")
|
|
limit := intParam(r.URL.Query().Get("items_count"), 50)
|
|
|
|
var query string
|
|
var args []any
|
|
if strings.HasPrefix(id, "0x") {
|
|
query = "SELECT * FROM transactions WHERE chain_id = ? AND block_hash = ? ORDER BY tx_index LIMIT ?"
|
|
args = []any{s.config.ChainID, hexToBytes(id), limit}
|
|
} else {
|
|
query = "SELECT * FROM transactions WHERE chain_id = ? AND block_number = ? ORDER BY tx_index LIMIT ?"
|
|
args = []any{s.config.ChainID, id, limit}
|
|
}
|
|
|
|
s.queryTxListCtx(w, r, ctx, query, args, limit)
|
|
}
|
|
|
|
// ---- Transactions ----
|
|
|
|
func (s *Service) handleListTransactions(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
q := r.URL.Query()
|
|
limit := intParam(q.Get("items_count"), 50)
|
|
|
|
query := "SELECT * FROM transactions WHERE chain_id = ? ORDER BY block_number DESC, tx_index DESC LIMIT ?"
|
|
args := []any{s.config.ChainID, limit + 1}
|
|
|
|
s.queryTxListCtx(w, r, ctx, query, args, limit)
|
|
}
|
|
|
|
func (s *Service) handleGetTransaction(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "transaction_hash")
|
|
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM transactions WHERE chain_id = ? AND hash = ? LIMIT 1",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
notFoundError(w, "transaction not found")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
txs, err := scanMaps(rows)
|
|
if err != nil || len(txs) == 0 {
|
|
notFoundError(w, "transaction not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, formatTx(txs[0]))
|
|
}
|
|
|
|
func (s *Service) handleTxTokenTransfers(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "transaction_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM token_transfers WHERE chain_id = ? AND transaction_hash = ? ORDER BY log_index",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
transfers, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(transfers))
|
|
for i, t := range transfers {
|
|
items[i] = formatTokenTransfer(t)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleTxInternalTxs(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "transaction_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM internal_transactions WHERE chain_id = ? AND transaction_hash = ? ORDER BY \"index\"",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
itxs, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(itxs))
|
|
for i, t := range itxs {
|
|
items[i] = formatInternalTx(t)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleTxLogs(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "transaction_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM logs WHERE chain_id = ? AND transaction_hash = ? ORDER BY \"index\"",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
logs, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(logs))
|
|
for i, l := range logs {
|
|
items[i] = formatLog(l)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleTxRawTrace(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, []any{})
|
|
}
|
|
|
|
// ---- Addresses ----
|
|
|
|
func (s *Service) handleListAddresses(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
limit := intParam(r.URL.Query().Get("items_count"), 50)
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM addresses WHERE chain_id = ? ORDER BY fetched_coin_balance DESC LIMIT ?",
|
|
s.config.ChainID, limit)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
addrs, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(addrs))
|
|
for i, a := range addrs {
|
|
items[i] = formatAddress(a)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleGetAddress(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM addresses WHERE chain_id = ? AND hash = ? LIMIT 1",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
notFoundError(w, "address not found")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
addrs, _ := scanMaps(rows)
|
|
if len(addrs) == 0 {
|
|
notFoundError(w, "address not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, formatAddress(addrs[0]))
|
|
}
|
|
|
|
func (s *Service) handleAddressTransactions(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
limit := intParam(r.URL.Query().Get("items_count"), 50)
|
|
addr := hexToBytes(hash)
|
|
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM transactions WHERE chain_id = ? AND (from_addr = ? OR to_addr = ?) ORDER BY block_number DESC, tx_index DESC LIMIT ?",
|
|
s.config.ChainID, addr, addr, limit+1)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
s.formatTxRows(w, rows, limit)
|
|
}
|
|
|
|
func (s *Service) handleAddressTokenTransfers(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
addr := hexToBytes(hash)
|
|
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM token_transfers WHERE chain_id = ? AND (from_addr = ? OR to_addr = ?) ORDER BY block_number DESC, log_index DESC LIMIT 50",
|
|
s.config.ChainID, addr, addr)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
transfers, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(transfers))
|
|
for i, t := range transfers {
|
|
items[i] = formatTokenTransfer(t)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleAddressInternalTxs(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
addr := hexToBytes(hash)
|
|
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM internal_transactions WHERE chain_id = ? AND (from_addr = ? OR to_addr = ?) ORDER BY block_number DESC LIMIT 50",
|
|
s.config.ChainID, addr, addr)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
itxs, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(itxs))
|
|
for i, t := range itxs {
|
|
items[i] = formatInternalTx(t)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleAddressLogs(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM logs WHERE chain_id = ? AND address_hash = ? ORDER BY block_number DESC LIMIT 50",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
logs, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(logs))
|
|
for i, l := range logs {
|
|
items[i] = formatLog(l)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleAddressTokens(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM address_current_token_balances WHERE chain_id = ? AND address_hash = ? ORDER BY value DESC LIMIT 100",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
bals, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(bals))
|
|
for i, b := range bals {
|
|
items[i] = map[string]any{
|
|
"token": map[string]any{
|
|
"address": bytesToHex(b["token_contract_addr"]),
|
|
"type": b["token_type"],
|
|
},
|
|
"value": fmt.Sprintf("%v", b["value"]),
|
|
"token_id": b["token_id"],
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleAddressCoinBalanceHistory(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM address_coin_balances WHERE chain_id = ? AND address_hash = ? ORDER BY block_number DESC LIMIT 50",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
bals, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(bals))
|
|
for i, b := range bals {
|
|
items[i] = map[string]any{
|
|
"block_number": b["block_number"],
|
|
"value": fmt.Sprintf("%v", b["value"]),
|
|
"delta": fmt.Sprintf("%v", b["delta"]),
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleAddressCoinBalanceByDay(w http.ResponseWriter, r *http.Request) {
|
|
s.handleAddressCoinBalanceHistory(w, r)
|
|
}
|
|
|
|
func (s *Service) handleAddressCounters(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
hash := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM addresses WHERE chain_id = ? AND hash = ? LIMIT 1",
|
|
s.config.ChainID, hexToBytes(hash))
|
|
if err != nil {
|
|
notFoundError(w, "address not found")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
addrs, _ := scanMaps(rows)
|
|
if len(addrs) == 0 {
|
|
notFoundError(w, "address not found")
|
|
return
|
|
}
|
|
|
|
a := addrs[0]
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"transactions_count": a["transactions_count"],
|
|
"token_transfers_count": a["token_transfers_count"],
|
|
"gas_usage_count": fmt.Sprintf("%v", a["gas_used"]),
|
|
"validations_count": 0,
|
|
})
|
|
}
|
|
|
|
// ---- Tokens ----
|
|
|
|
func (s *Service) handleListTokens(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
limit := intParam(r.URL.Query().Get("items_count"), 50)
|
|
q := r.URL.Query()
|
|
|
|
query := "SELECT * FROM tokens WHERE chain_id = ?"
|
|
args := []any{s.config.ChainID}
|
|
|
|
if t := q.Get("type"); t != "" {
|
|
query += " AND type = ?"
|
|
args = append(args, t)
|
|
}
|
|
query += " ORDER BY holder_count DESC LIMIT ?"
|
|
args = append(args, limit)
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
tokens, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(tokens))
|
|
for i, t := range tokens {
|
|
items[i] = formatToken(t)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleGetToken(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
addr := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM tokens WHERE chain_id = ? AND contract_addr = ? LIMIT 1",
|
|
s.config.ChainID, hexToBytes(addr))
|
|
if err != nil {
|
|
notFoundError(w, "token not found")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
tokens, _ := scanMaps(rows)
|
|
if len(tokens) == 0 {
|
|
notFoundError(w, "token not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, formatToken(tokens[0]))
|
|
}
|
|
|
|
func (s *Service) handleTokenTransfers(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
addr := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM token_transfers WHERE chain_id = ? AND token_contract_addr = ? ORDER BY block_number DESC LIMIT 50",
|
|
s.config.ChainID, hexToBytes(addr))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
transfers, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(transfers))
|
|
for i, t := range transfers {
|
|
items[i] = formatTokenTransfer(t)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleTokenHolders(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
addr := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM address_current_token_balances WHERE chain_id = ? AND token_contract_addr = ? ORDER BY value DESC LIMIT 50",
|
|
s.config.ChainID, hexToBytes(addr))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
bals, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(bals))
|
|
for i, b := range bals {
|
|
items[i] = map[string]any{
|
|
"address": map[string]any{"hash": bytesToHex(b["address_hash"])},
|
|
"value": fmt.Sprintf("%v", b["value"]),
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleTokenInstances(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
}
|
|
|
|
func (s *Service) handleTokenInstance(w http.ResponseWriter, r *http.Request) {
|
|
notFoundError(w, "not found")
|
|
}
|
|
|
|
// ---- Smart Contracts ----
|
|
|
|
func (s *Service) handleListContracts(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM smart_contracts WHERE chain_id = ? ORDER BY inserted_at DESC LIMIT 50",
|
|
s.config.ChainID)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
contracts, _ := scanMaps(rows)
|
|
items := make([]map[string]any, len(contracts))
|
|
for i, c := range contracts {
|
|
items[i] = formatContract(c)
|
|
}
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleGetContract(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
addr := chi.URLParam(r, "address_hash")
|
|
rows, err := s.db.QueryContext(ctx, "SELECT * FROM smart_contracts WHERE chain_id = ? AND address_hash = ? LIMIT 1",
|
|
s.config.ChainID, hexToBytes(addr))
|
|
if err != nil {
|
|
notFoundError(w, "contract not found")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
contracts, _ := scanMaps(rows)
|
|
if len(contracts) == 0 {
|
|
notFoundError(w, "contract not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, formatContract(contracts[0]))
|
|
}
|
|
|
|
func (s *Service) handleVerifyContract(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "queued"})
|
|
}
|
|
|
|
// ---- Search ----
|
|
|
|
func (s *Service) handleSearch(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
q := r.URL.Query().Get("q")
|
|
if q == "" {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
|
|
var items []map[string]any
|
|
|
|
// Transaction hash
|
|
if strings.HasPrefix(q, "0x") && len(q) == 66 {
|
|
if row := s.queryOneCtx(ctx, "transactions", "hash", hexToBytes(q)); row != nil {
|
|
items = append(items, map[string]any{
|
|
"type": "transaction",
|
|
"transaction_hash": bytesToHex(row["hash"]),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Address
|
|
if strings.HasPrefix(q, "0x") && len(q) == 42 {
|
|
items = append(items, map[string]any{
|
|
"type": "address",
|
|
"address_hash": strings.ToLower(q),
|
|
})
|
|
}
|
|
|
|
// Block number
|
|
if n, err := strconv.ParseInt(q, 10, 64); err == nil {
|
|
var count int
|
|
s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM blocks WHERE chain_id = ? AND number = ?", s.config.ChainID, n).Scan(&count)
|
|
if count > 0 {
|
|
items = append(items, map[string]any{
|
|
"type": "block",
|
|
"block_number": n,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Token name/symbol search
|
|
if !strings.HasPrefix(q, "0x") {
|
|
escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(q)
|
|
rows, err := s.db.QueryContext(ctx,
|
|
"SELECT * FROM tokens WHERE chain_id = ? AND (name LIKE ? ESCAPE '\\' OR symbol LIKE ? ESCAPE '\\') ORDER BY holder_count DESC LIMIT 5",
|
|
s.config.ChainID, "%"+escaped+"%", "%"+escaped+"%")
|
|
if err == nil {
|
|
defer rows.Close()
|
|
tokens, _ := scanMaps(rows)
|
|
for _, t := range tokens {
|
|
items = append(items, map[string]any{
|
|
"type": "token",
|
|
"name": t["name"],
|
|
"symbol": t["symbol"],
|
|
"address_hash": bytesToHex(t["contract_addr"]),
|
|
"token_type": t["type"],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nil})
|
|
}
|
|
|
|
func (s *Service) handleSearchRedirect(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query().Get("q")
|
|
if strings.HasPrefix(q, "0x") && len(q) == 66 {
|
|
writeJSON(w, http.StatusOK, map[string]any{"redirect": true, "type": "transaction", "parameter": q})
|
|
return
|
|
}
|
|
if strings.HasPrefix(q, "0x") && len(q) == 42 {
|
|
writeJSON(w, http.StatusOK, map[string]any{"redirect": true, "type": "address", "parameter": q})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"redirect": false})
|
|
}
|
|
|
|
// ---- Stats ----
|
|
|
|
func (s *Service) handleStats(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
var blockCount, txCount, addrCount int
|
|
s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM blocks WHERE chain_id = ?", s.config.ChainID).Scan(&blockCount)
|
|
s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM transactions WHERE chain_id = ?", s.config.ChainID).Scan(&txCount)
|
|
s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM addresses WHERE chain_id = ?", s.config.ChainID).Scan(&addrCount)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"total_blocks": blockCount,
|
|
"total_transactions": txCount,
|
|
"total_addresses": addrCount,
|
|
"coin_price": nil,
|
|
"coin_price_change_percentage": nil,
|
|
"total_gas_used": "0",
|
|
"average_block_time": 0,
|
|
"market_cap": "0",
|
|
"network_utilization_percentage": 0,
|
|
"coin_image": nil,
|
|
})
|
|
}
|
|
|
|
func (s *Service) handleChartTransactions(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"chart_data": []any{}})
|
|
}
|
|
|
|
func (s *Service) handleChartMarket(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"chart_data": []any{}})
|
|
}
|
|
|
|
func (s *Service) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"healthy": s.db.PingContext(ctx) == nil,
|
|
"chain_id": s.config.ChainID,
|
|
"chain_name": s.config.ChainName,
|
|
})
|
|
}
|
|
|
|
// ---- Query Helpers ----
|
|
|
|
func (s *Service) queryOneCtx(ctx context.Context, table, col string, val any) map[string]any {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
fmt.Sprintf("SELECT * FROM %s WHERE chain_id = ? AND %s = ? LIMIT 1", table, col),
|
|
s.config.ChainID, val)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rows.Close()
|
|
maps, _ := scanMaps(rows)
|
|
if len(maps) == 0 {
|
|
return nil
|
|
}
|
|
return maps[0]
|
|
}
|
|
|
|
func (s *Service) queryTxListCtx(w http.ResponseWriter, r *http.Request, ctx context.Context, query string, args []any, limit int) {
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, emptyPage())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
s.formatTxRows(w, rows, limit)
|
|
}
|
|
|
|
func (s *Service) formatTxRows(w http.ResponseWriter, rows *sql.Rows, limit int) {
|
|
txs, _ := scanMaps(rows)
|
|
|
|
var nextPage any
|
|
if len(txs) > limit {
|
|
last := txs[limit-1]
|
|
nextPage = map[string]any{
|
|
"block_number": last["block_number"],
|
|
"index": last["tx_index"],
|
|
"items_count": limit,
|
|
}
|
|
txs = txs[:limit]
|
|
}
|
|
|
|
items := make([]map[string]any, len(txs))
|
|
for i, t := range txs {
|
|
items[i] = formatTx(t)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, paginatedResponse{Items: items, NextPageParams: nextPage})
|
|
}
|
|
|
|
func intParam(s string, fallback int) int {
|
|
if n, err := strconv.Atoi(s); err == nil && n > 0 {
|
|
if n > 250 {
|
|
n = 250
|
|
}
|
|
return n
|
|
}
|
|
return fallback
|
|
}
|