Files
explorer-monorepo/backend/api/rest/transactions.go
defiQUG bdae5a9f6e feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths.
- Frontend wallet and liquidity surfaces; MetaMask token list alignment.
- Deployment docs, verification scripts, address inventory updates.

Check: go build ./... under backend/ (pass).
Made-with: Cursor
2026-04-07 23:22:12 -07:00

254 lines
6.5 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 {
writeMethodNotAllowed(w)
return
}
if !s.requireDB(w) {
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 != "" {
if !isValidAddress(fromAddress) {
writeValidationError(w, ErrInvalidAddress)
return
}
query += fmt.Sprintf(" AND LOWER(from_address) = $%d", argIndex)
args = append(args, normalizeAddress(fromAddress))
argIndex++
}
if toAddress := r.URL.Query().Get("to_address"); toAddress != "" {
if !isValidAddress(toAddress) {
writeValidationError(w, ErrInvalidAddress)
return
}
query += fmt.Sprintf(" AND LOWER(to_address) = $%d", argIndex)
args = append(args, normalizeAddress(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 {
writeInternalError(w, "Database error")
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) {
if !s.requireDB(w) {
return
}
hash = normalizeHash(hash)
// 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 {
writeNotFound(w, "Transaction")
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,
})
}