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

237 lines
6.3 KiB
Go

package rest
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
)
// handleListTransactions handles GET /api/v1/transactions
func (s *Server) handleListTransactions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate pagination
page, pageSize, err := validatePagination(
r.URL.Query().Get("page"),
r.URL.Query().Get("page_size"),
)
if err != nil {
writeValidationError(w, err)
return
}
offset := (page - 1) * pageSize
query := `
SELECT t.chain_id, t.hash, t.block_number, t.transaction_index, t.from_address, t.to_address,
t.value, t.gas_price, t.gas_used, t.status, t.created_at, t.timestamp_iso
FROM transactions t
WHERE t.chain_id = $1
`
args := []interface{}{s.chainID}
argIndex := 2
// Add filters
if blockNumber := r.URL.Query().Get("block_number"); blockNumber != "" {
if bn, err := strconv.ParseInt(blockNumber, 10, 64); err == nil {
query += fmt.Sprintf(" AND block_number = $%d", argIndex)
args = append(args, bn)
argIndex++
}
}
if fromAddress := r.URL.Query().Get("from_address"); fromAddress != "" {
query += fmt.Sprintf(" AND from_address = $%d", argIndex)
args = append(args, fromAddress)
argIndex++
}
if toAddress := r.URL.Query().Get("to_address"); toAddress != "" {
query += fmt.Sprintf(" AND to_address = $%d", argIndex)
args = append(args, toAddress)
argIndex++
}
query += " ORDER BY block_number DESC, transaction_index DESC"
query += fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIndex, argIndex+1)
args = append(args, pageSize, offset)
// Add query timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rows, err := s.db.Query(ctx, query, args...)
if err != nil {
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
return
}
defer rows.Close()
transactions := []map[string]interface{}{}
for rows.Next() {
var chainID, blockNumber, transactionIndex int
var hash, fromAddress string
var toAddress sql.NullString
var value string
var gasPrice, gasUsed sql.NullInt64
var status sql.NullInt64
var createdAt time.Time
var timestampISO sql.NullString
if err := rows.Scan(&chainID, &hash, &blockNumber, &transactionIndex, &fromAddress, &toAddress,
&value, &gasPrice, &gasUsed, &status, &createdAt, &timestampISO); err != nil {
continue
}
tx := map[string]interface{}{
"chain_id": chainID,
"hash": hash,
"block_number": blockNumber,
"transaction_index": transactionIndex,
"from_address": fromAddress,
"value": value,
"created_at": createdAt,
}
if timestampISO.Valid {
tx["timestamp_iso"] = timestampISO.String
}
if toAddress.Valid {
tx["to_address"] = toAddress.String
}
if gasPrice.Valid {
tx["gas_price"] = gasPrice.Int64
}
if gasUsed.Valid {
tx["gas_used"] = gasUsed.Int64
}
if status.Valid {
tx["status"] = status.Int64
}
transactions = append(transactions, tx)
}
response := map[string]interface{}{
"data": transactions,
"meta": map[string]interface{}{
"pagination": map[string]interface{}{
"page": page,
"page_size": pageSize,
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// handleGetTransactionByHash handles GET /api/v1/transactions/{chain_id}/{hash}
func (s *Server) handleGetTransactionByHash(w http.ResponseWriter, r *http.Request, hash string) {
// Validate hash format (already validated in routes.go, but double-check)
if !isValidHash(hash) {
writeValidationError(w, ErrInvalidHash)
return
}
// Add query timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
query := `
SELECT chain_id, hash, block_number, block_hash, transaction_index,
from_address, to_address, value, gas_price, max_fee_per_gas,
max_priority_fee_per_gas, gas_limit, gas_used, nonce, input_data,
status, contract_address, cumulative_gas_used, effective_gas_price,
created_at, timestamp_iso
FROM transactions
WHERE chain_id = $1 AND hash = $2
`
var chainID, blockNumber, transactionIndex int
var txHash, blockHash, fromAddress string
var toAddress sql.NullString
var value string
var gasPrice, maxFeePerGas, maxPriorityFeePerGas, gasLimit, gasUsed, nonce sql.NullInt64
var inputData sql.NullString
var status sql.NullInt64
var contractAddress sql.NullString
var cumulativeGasUsed int64
var effectiveGasPrice sql.NullInt64
var createdAt time.Time
var timestampISO sql.NullString
err := s.db.QueryRow(ctx, query, s.chainID, hash).Scan(
&chainID, &txHash, &blockNumber, &blockHash, &transactionIndex,
&fromAddress, &toAddress, &value, &gasPrice, &maxFeePerGas,
&maxPriorityFeePerGas, &gasLimit, &gasUsed, &nonce, &inputData,
&status, &contractAddress, &cumulativeGasUsed, &effectiveGasPrice,
&createdAt, &timestampISO,
)
if err != nil {
http.Error(w, fmt.Sprintf("Transaction not found: %v", err), http.StatusNotFound)
return
}
tx := map[string]interface{}{
"chain_id": chainID,
"hash": txHash,
"block_number": blockNumber,
"block_hash": blockHash,
"transaction_index": transactionIndex,
"from_address": fromAddress,
"value": value,
"gas_limit": gasLimit.Int64,
"cumulative_gas_used": cumulativeGasUsed,
"created_at": createdAt,
}
if timestampISO.Valid {
tx["timestamp_iso"] = timestampISO.String
}
if toAddress.Valid {
tx["to_address"] = toAddress.String
}
if gasPrice.Valid {
tx["gas_price"] = gasPrice.Int64
}
if maxFeePerGas.Valid {
tx["max_fee_per_gas"] = maxFeePerGas.Int64
}
if maxPriorityFeePerGas.Valid {
tx["max_priority_fee_per_gas"] = maxPriorityFeePerGas.Int64
}
if gasUsed.Valid {
tx["gas_used"] = gasUsed.Int64
}
if nonce.Valid {
tx["nonce"] = nonce.Int64
}
if inputData.Valid {
tx["input_data"] = inputData.String
}
if status.Valid {
tx["status"] = status.Int64
}
if contractAddress.Valid {
tx["contract_address"] = contractAddress.String
}
if effectiveGasPrice.Valid {
tx["effective_gas_price"] = effectiveGasPrice.Int64
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"data": tx,
})
}