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, ×tampISO); 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, ×tampISO, ) 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, }) }