package rest import ( "context" "database/sql" "encoding/json" "fmt" "math/big" "net/http" "strconv" "strings" "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 { writeMethodNotAllowed(w) return } if !s.requireDB(w) { 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 interface{} 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 { txObjects, err := s.loadEtherscanBlockTransactions(ctx, blockNumber) if err != nil { response = EtherscanResponse{ Status: "0", Message: "Error", Result: nil, } break } transactions = txObjects } else { txHashes, err := s.loadEtherscanBlockTransactionHashes(ctx, blockNumber) if err != nil { response = EtherscanResponse{ Status: "0", Message: "Error", Result: nil, } break } transactions = txHashes } 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) } func (s *Server) loadEtherscanBlockTransactionHashes(ctx context.Context, blockNumber int64) ([]string, error) { rows, err := s.db.Query(ctx, ` SELECT hash FROM transactions WHERE chain_id = $1 AND block_number = $2 ORDER BY transaction_index `, s.chainID, blockNumber) if err != nil { return nil, err } defer rows.Close() hashes := make([]string, 0) for rows.Next() { var txHash string if err := rows.Scan(&txHash); err != nil { return nil, err } hashes = append(hashes, txHash) } return hashes, rows.Err() } func (s *Server) loadEtherscanBlockTransactions(ctx context.Context, blockNumber int64) ([]map[string]interface{}, error) { rows, err := s.db.Query(ctx, ` SELECT hash, block_hash, transaction_index, from_address, to_address, value::text, COALESCE(gas_price, 0), gas_limit, nonce, COALESCE(input_data, '') FROM transactions WHERE chain_id = $1 AND block_number = $2 ORDER BY transaction_index `, s.chainID, blockNumber) if err != nil { return nil, err } defer rows.Close() transactions := make([]map[string]interface{}, 0) for rows.Next() { var hash, blockHash, fromAddress, value, inputData string var toAddress sql.NullString var transactionIndex int var gasPrice, gasLimit, nonce int64 if err := rows.Scan(&hash, &blockHash, &transactionIndex, &fromAddress, &toAddress, &value, &gasPrice, &gasLimit, &nonce, &inputData); err != nil { return nil, err } tx := map[string]interface{}{ "hash": hash, "blockHash": blockHash, "blockNumber": fmt.Sprintf("0x%x", blockNumber), "transactionIndex": fmt.Sprintf("0x%x", transactionIndex), "from": fromAddress, "value": decimalStringToHex(value), "gasPrice": fmt.Sprintf("0x%x", gasPrice), "gas": fmt.Sprintf("0x%x", gasLimit), "nonce": fmt.Sprintf("0x%x", nonce), "input": normalizeHexInput(inputData), } if toAddress.Valid && toAddress.String != "" { tx["to"] = toAddress.String } else { tx["to"] = nil } transactions = append(transactions, tx) } return transactions, rows.Err() } func decimalStringToHex(value string) string { parsed, ok := new(big.Int).SetString(value, 10) if !ok { return "0x0" } return "0x" + parsed.Text(16) } func normalizeHexInput(input string) string { trimmed := strings.TrimSpace(input) if trimmed == "" { return "0x" } if strings.HasPrefix(trimmed, "0x") { return trimmed } return "0x" + trimmed }