Files
explorer-monorepo/backend/api/track1/endpoints.go
2026-03-02 12:14:13 -08:00

394 lines
10 KiB
Go

package track1
import (
"encoding/json"
"fmt"
"math/big"
"net/http"
"strconv"
"strings"
"time"
"github.com/explorer/backend/libs/go-rpc-gateway"
)
// Server handles Track 1 endpoints (uses RPC gateway from lib)
type Server struct {
rpcGateway *gateway.RPCGateway
}
// NewServer creates a new Track 1 server
func NewServer(rpcGateway *gateway.RPCGateway) *Server {
return &Server{
rpcGateway: rpcGateway,
}
}
// HandleLatestBlocks handles GET /api/v1/track1/blocks/latest
func (s *Server) HandleLatestBlocks(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
limit := 10
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
limit = l
}
}
// Get latest block number
blockNumResp, err := s.rpcGateway.GetBlockNumber(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "rpc_error", err.Error())
return
}
blockNumHex, ok := blockNumResp.Result.(string)
if !ok {
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block number response")
return
}
// Parse block number
blockNum, err := hexToInt(blockNumHex)
if err != nil {
writeError(w, http.StatusInternalServerError, "parse_error", err.Error())
return
}
// Fetch blocks
blocks := []map[string]interface{}{}
for i := 0; i < limit && blockNum-int64(i) >= 0; i++ {
blockNumStr := fmt.Sprintf("0x%x", blockNum-int64(i))
blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, false)
if err != nil {
continue // Skip failed blocks
}
blockData, ok := blockResp.Result.(map[string]interface{})
if !ok {
continue
}
// Transform to our format
block := transformBlock(blockData)
blocks = append(blocks, block)
}
response := map[string]interface{}{
"data": blocks,
"pagination": map[string]interface{}{
"page": 1,
"limit": limit,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleLatestTransactions handles GET /api/v1/track1/txs/latest
func (s *Server) HandleLatestTransactions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
limit := 10
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
limit = l
}
}
// Get latest block number
blockNumResp, err := s.rpcGateway.GetBlockNumber(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "rpc_error", err.Error())
return
}
blockNumHex, ok := blockNumResp.Result.(string)
if !ok {
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block number response")
return
}
blockNum, err := hexToInt(blockNumHex)
if err != nil {
writeError(w, http.StatusInternalServerError, "parse_error", err.Error())
return
}
// Fetch transactions from recent blocks
transactions := []map[string]interface{}{}
for i := 0; i < 20 && len(transactions) < limit && blockNum-int64(i) >= 0; i++ {
blockNumStr := fmt.Sprintf("0x%x", blockNum-int64(i))
blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, true)
if err != nil {
continue
}
blockData, ok := blockResp.Result.(map[string]interface{})
if !ok {
continue
}
txs, ok := blockData["transactions"].([]interface{})
if !ok {
continue
}
for _, tx := range txs {
if len(transactions) >= limit {
break
}
txData, ok := tx.(map[string]interface{})
if !ok {
continue
}
transactions = append(transactions, transformTransaction(txData))
}
}
response := map[string]interface{}{
"data": transactions,
"pagination": map[string]interface{}{
"page": 1,
"limit": limit,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleBlockDetail handles GET /api/v1/track1/block/:number
func (s *Server) HandleBlockDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/block/")
blockNumStr := fmt.Sprintf("0x%x", parseBlockNumber(path))
blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, false)
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "Block not found")
return
}
blockData, ok := blockResp.Result.(map[string]interface{})
if !ok {
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block response")
return
}
response := map[string]interface{}{
"data": transformBlock(blockData),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleTransactionDetail handles GET /api/v1/track1/tx/:hash
func (s *Server) HandleTransactionDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/tx/")
txHash := path
txResp, err := s.rpcGateway.GetTransactionByHash(r.Context(), txHash)
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "Transaction not found")
return
}
txData, ok := txResp.Result.(map[string]interface{})
if !ok {
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid transaction response")
return
}
response := map[string]interface{}{
"data": transformTransaction(txData),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleAddressBalance handles GET /api/v1/track1/address/:addr/balance
func (s *Server) HandleAddressBalance(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/address/")
parts := strings.Split(path, "/")
if len(parts) < 2 || parts[1] != "balance" {
writeError(w, http.StatusBadRequest, "bad_request", "Invalid path")
return
}
address := parts[0]
balanceResp, err := s.rpcGateway.GetBalance(r.Context(), address, "latest")
if err != nil {
writeError(w, http.StatusInternalServerError, "rpc_error", err.Error())
return
}
balanceHex, ok := balanceResp.Result.(string)
if !ok {
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid balance response")
return
}
balance, err := hexToBigInt(balanceHex)
if err != nil {
writeError(w, http.StatusInternalServerError, "parse_error", err.Error())
return
}
response := map[string]interface{}{
"data": map[string]interface{}{
"address": address,
"balance": balance.String(),
"balance_wei": balance.String(),
"balance_ether": weiToEther(balance),
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleBridgeStatus handles GET /api/v1/track1/bridge/status
func (s *Server) HandleBridgeStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
// Return bridge status (simplified - in production, query bridge contracts)
response := map[string]interface{}{
"data": map[string]interface{}{
"status": "operational",
"chains": map[string]interface{}{
"138": map[string]interface{}{
"name": "Defi Oracle Meta Mainnet",
"status": "operational",
"last_sync": time.Now().UTC().Format(time.RFC3339),
},
"1": map[string]interface{}{
"name": "Ethereum Mainnet",
"status": "operational",
"last_sync": time.Now().UTC().Format(time.RFC3339),
},
},
"total_transfers_24h": 150,
"total_volume_24h": "5000000000000000000000",
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// Helper functions
func writeError(w http.ResponseWriter, statusCode int, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"code": code,
"message": message,
},
})
}
func hexToInt(hex string) (int64, error) {
hex = strings.TrimPrefix(hex, "0x")
return strconv.ParseInt(hex, 16, 64)
}
func parseBlockNumber(s string) int64 {
num, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0
}
return num
}
func transformBlock(blockData map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"number": parseHexField(blockData["number"]),
"hash": blockData["hash"],
"parent_hash": blockData["parentHash"],
"timestamp": parseHexTimestamp(blockData["timestamp"]),
"transaction_count": countTransactions(blockData["transactions"]),
"gas_used": parseHexField(blockData["gasUsed"]),
"gas_limit": parseHexField(blockData["gasLimit"]),
"miner": blockData["miner"],
}
}
func transformTransaction(txData map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"hash": txData["hash"],
"from": txData["from"],
"to": txData["to"],
"value": txData["value"],
"block_number": parseHexField(txData["blockNumber"]),
"timestamp": parseHexTimestamp(txData["timestamp"]),
}
}
func parseHexField(field interface{}) interface{} {
if str, ok := field.(string); ok {
if num, err := hexToInt(str); err == nil {
return num
}
}
return field
}
func parseHexTimestamp(field interface{}) string {
if str, ok := field.(string); ok {
if num, err := hexToInt(str); err == nil {
return time.Unix(num, 0).Format(time.RFC3339)
}
}
return ""
}
func countTransactions(txs interface{}) int {
if txsList, ok := txs.([]interface{}); ok {
return len(txsList)
}
return 0
}
func hexToBigInt(hex string) (*big.Int, error) {
hex = strings.TrimPrefix(hex, "0x")
bigInt := new(big.Int)
bigInt, ok := bigInt.SetString(hex, 16)
if !ok {
return nil, fmt.Errorf("invalid hex number")
}
return bigInt, nil
}
func weiToEther(wei *big.Int) string {
ether := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18))
return ether.Text('f', 18)
}