Files
explorer-monorepo/backend/api/rest/etherscan.go

216 lines
5.1 KiB
Go

package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
)
// handleEtherscanAPI handles GET /api?module=...&action=...
// This provides Etherscan-compatible API endpoints
func (s *Server) handleEtherscanAPI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
module := r.URL.Query().Get("module")
action := r.URL.Query().Get("action")
// Etherscan-compatible response structure
type EtherscanResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Result interface{} `json:"result"`
}
// Validate required parameters
if module == "" || action == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
response := EtherscanResponse{
Status: "0",
Message: "Params 'module' and 'action' are required parameters",
Result: nil,
}
json.NewEncoder(w).Encode(response)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var response EtherscanResponse
switch module {
case "block":
switch action {
case "eth_block_number":
// Get latest block number
var blockNumber int64
err := s.db.QueryRow(ctx,
`SELECT MAX(number) FROM blocks WHERE chain_id = $1`,
s.chainID,
).Scan(&blockNumber)
if err != nil {
response = EtherscanResponse{
Status: "0",
Message: "Error",
Result: "0x0",
}
} else {
response = EtherscanResponse{
Status: "1",
Message: "OK",
Result: fmt.Sprintf("0x%x", blockNumber),
}
}
case "eth_get_block_by_number":
tag := r.URL.Query().Get("tag")
boolean := r.URL.Query().Get("boolean") == "true"
// Parse block number from tag (can be "latest", "0x...", or decimal)
var blockNumber int64
if tag == "latest" {
err := s.db.QueryRow(ctx,
`SELECT MAX(number) FROM blocks WHERE chain_id = $1`,
s.chainID,
).Scan(&blockNumber)
if err != nil {
response = EtherscanResponse{
Status: "0",
Message: "Error",
Result: nil,
}
break
}
} else if len(tag) > 2 && tag[:2] == "0x" {
// Hex format
parsed, err := strconv.ParseInt(tag[2:], 16, 64)
if err != nil {
response = EtherscanResponse{
Status: "0",
Message: "Invalid block number",
Result: nil,
}
break
}
blockNumber = parsed
} else {
// Decimal format
parsed, err := strconv.ParseInt(tag, 10, 64)
if err != nil {
response = EtherscanResponse{
Status: "0",
Message: "Invalid block number",
Result: nil,
}
break
}
blockNumber = parsed
}
// Get block data
var hash, parentHash, miner string
var timestamp time.Time
var transactionCount int
var gasUsed, gasLimit int64
var transactions []string
query := `
SELECT hash, parent_hash, timestamp, miner, transaction_count, gas_used, gas_limit
FROM blocks
WHERE chain_id = $1 AND number = $2
`
err := s.db.QueryRow(ctx, query, s.chainID, blockNumber).Scan(
&hash, &parentHash, &timestamp, &miner, &transactionCount, &gasUsed, &gasLimit,
)
if err != nil {
response = EtherscanResponse{
Status: "0",
Message: "Block not found",
Result: nil,
}
break
}
// If boolean is true, get full transaction objects
if boolean {
txQuery := `
SELECT hash FROM transactions
WHERE chain_id = $1 AND block_number = $2
ORDER BY transaction_index
`
rows, err := s.db.Query(ctx, txQuery, s.chainID, blockNumber)
if err == nil {
defer rows.Close()
for rows.Next() {
var txHash string
if err := rows.Scan(&txHash); err == nil {
transactions = append(transactions, txHash)
}
}
}
} else {
// Just get transaction hashes
txQuery := `
SELECT hash FROM transactions
WHERE chain_id = $1 AND block_number = $2
ORDER BY transaction_index
`
rows, err := s.db.Query(ctx, txQuery, s.chainID, blockNumber)
if err == nil {
defer rows.Close()
for rows.Next() {
var txHash string
if err := rows.Scan(&txHash); err == nil {
transactions = append(transactions, txHash)
}
}
}
}
blockResult := map[string]interface{}{
"number": fmt.Sprintf("0x%x", blockNumber),
"hash": hash,
"parentHash": parentHash,
"timestamp": fmt.Sprintf("0x%x", timestamp.Unix()),
"miner": miner,
"transactions": transactions,
"transactionCount": fmt.Sprintf("0x%x", transactionCount),
"gasUsed": fmt.Sprintf("0x%x", gasUsed),
"gasLimit": fmt.Sprintf("0x%x", gasLimit),
}
response = EtherscanResponse{
Status: "1",
Message: "OK",
Result: blockResult,
}
default:
response = EtherscanResponse{
Status: "0",
Message: "Invalid action",
Result: nil,
}
}
default:
response = EtherscanResponse{
Status: "0",
Message: "Invalid module",
Result: nil,
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}