Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
215
backend/api/rest/etherscan.go
Normal file
215
backend/api/rest/etherscan.go
Normal file
@@ -0,0 +1,215 @@
|
||||
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, ×tamp, &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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user