go-ethereum/cmd/pushtx/main_test.go
copilot-swe-agent[bot] 84f37d3915 cmd/pushtx: add tx cost display to catch insufficient funds before broadcast
Co-authored-by: drQedwards <213266729+drQedwards@users.noreply.github.com>
2026-03-12 16:24:50 +00:00

339 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"crypto/ecdsa"
"encoding/json"
"io"
"math/big"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
// signedTestTx returns a signed legacy transaction and its hex encoding.
func signedTestTx(t *testing.T) (*types.Transaction, string) {
t.Helper()
key, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
return signedTestTxWithKey(t, key)
}
func signedTestTxWithKey(t *testing.T, key *ecdsa.PrivateKey) (*types.Transaction, string) {
t.Helper()
tx := types.NewTx(&types.LegacyTx{
Nonce: 6,
GasPrice: big.NewInt(1_000_000_000), // 1 Gwei real networks reject gas price 0
Gas: 21055,
To: addrPtr(common.HexToAddress("0x78b5290269740033b05bd8d71c97331295eb5918")),
Value: new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH
})
signer := types.NewEIP155Signer(big.NewInt(1))
signed, err := types.SignTx(tx, signer, key)
if err != nil {
t.Fatal(err)
}
data, err := signed.MarshalBinary()
if err != nil {
t.Fatal(err)
}
return signed, hexutil.Encode(data)
}
func addrPtr(a common.Address) *common.Address { return &a }
// fakeRPC starts an HTTP server that responds to eth_sendRawTransaction.
func fakeRPC(t *testing.T, wantErr bool) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
ID json.RawMessage `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Method != "eth_sendRawTransaction" {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": req.ID,
"error": map[string]interface{}{"code": -32601, "message": "method not found"},
})
return
}
if wantErr {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": req.ID,
"error": map[string]interface{}{"code": -32000, "message": "already known"},
})
return
}
// Decode the raw tx to return its hash as the result.
var hexData string
if err := json.Unmarshal(req.Params[0], &hexData); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
rawBytes, err := hexutil.Decode(hexData)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var tx types.Transaction
if err := tx.UnmarshalBinary(rawBytes); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": req.ID,
"result": tx.Hash().Hex(),
})
}))
}
func TestRunSuccess(t *testing.T) {
srv := fakeRPC(t, false)
defer srv.Close()
_, txHex := signedTestTx(t)
err := run([]string{"--rpc", srv.URL, txHex}, strings.NewReader(""))
if err != nil {
t.Fatal("unexpected error:", err)
}
}
func TestRunFromStdin(t *testing.T) {
srv := fakeRPC(t, false)
defer srv.Close()
_, txHex := signedTestTx(t)
err := run([]string{"--rpc", srv.URL}, strings.NewReader(txHex))
if err != nil {
t.Fatal("unexpected error:", err)
}
}
func TestRunRPCError(t *testing.T) {
srv := fakeRPC(t, true)
defer srv.Close()
_, txHex := signedTestTx(t)
// Capture stdout raw hex should still be printed on RPC failure.
oldStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
runErr := run([]string{"--rpc", srv.URL, txHex}, strings.NewReader(""))
w.Close()
os.Stdout = oldStdout
if runErr == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(runErr.Error(), "already known") {
t.Fatalf("unexpected error message: %v", runErr)
}
out, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(out), "Raw tx: 0x") {
t.Fatal("expected raw hex in output even on RPC error")
}
}
func TestRunNoInput(t *testing.T) {
err := run(nil, strings.NewReader(""))
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "no transaction data") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunBadHex(t *testing.T) {
err := run([]string{"not-hex-data"}, strings.NewReader(""))
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "invalid hex") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunBadTx(t *testing.T) {
err := run([]string{"0xdeadbeef"}, strings.NewReader(""))
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "decoding transaction") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunHelp(t *testing.T) {
err := run([]string{"--help"}, strings.NewReader(""))
if err != nil {
t.Fatal("unexpected error:", err)
}
}
func TestRunUnknownFlag(t *testing.T) {
err := run([]string{"--unknown"}, strings.NewReader(""))
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "unknown flag") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunExtraArgs(t *testing.T) {
err := run([]string{"0xaa", "0xbb"}, strings.NewReader(""))
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "unexpected argument") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestFormatWei(t *testing.T) {
tests := []struct {
wei *big.Int
want string
}{
{nil, "0 wei (0 ETH)"},
{big.NewInt(0), "0 wei (0 ETH)"},
{big.NewInt(1e18), "1000000000000000000 wei (1.000000000000000000 ETH)"},
{new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), "10000000000000000000 wei (10.000000000000000000 ETH)"},
}
for _, tt := range tests {
got := formatWei(tt.wei)
if got != tt.want {
t.Errorf("formatWei(%v) = %q, want %q", tt.wei, got, tt.want)
}
}
}
func TestFormatGwei(t *testing.T) {
tests := []struct {
wei *big.Int
want string
}{
{nil, "0 wei (0 Gwei)"},
{big.NewInt(0), "0 wei (0 Gwei)"},
{big.NewInt(1_000_000_000), "1000000000 wei (1.000000000 Gwei)"},
{big.NewInt(20_000_000_000), "20000000000 wei (20.000000000 Gwei)"},
}
for _, tt := range tests {
got := formatGwei(tt.wei)
if got != tt.want {
t.Errorf("formatGwei(%v) = %q, want %q", tt.wei, got, tt.want)
}
}
}
func TestTxCost(t *testing.T) {
tx := types.NewTx(&types.LegacyTx{
GasPrice: big.NewInt(1_000_000_000), // 1 Gwei
Gas: 21055,
Value: new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH
})
got := txCost(tx)
// Expected: 10 ETH + 21055 * 1 Gwei = 10000000000000000000 + 21055000000000 = 10000021055000000000
want, _ := new(big.Int).SetString("10000021055000000000", 10)
if got.Cmp(want) != 0 {
t.Errorf("txCost = %s, want %s", got, want)
}
}
func TestRunEqualsSyntax(t *testing.T) {
srv := fakeRPC(t, false)
defer srv.Close()
_, txHex := signedTestTx(t)
err := run([]string{"--rpc=" + srv.URL, txHex}, strings.NewReader(""))
if err != nil {
t.Fatal("unexpected error:", err)
}
}
func TestRunOutputEndsWithRawHex(t *testing.T) {
srv := fakeRPC(t, false)
defer srv.Close()
_, txHex := signedTestTx(t)
// Capture stdout to verify "Raw tx:" appears in output.
oldStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
runErr := run([]string{"--rpc", srv.URL, txHex}, strings.NewReader(""))
w.Close()
os.Stdout = oldStdout
if runErr != nil {
t.Fatal("unexpected error:", runErr)
}
out, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
lastLine := lines[len(lines)-1]
// The last line must be the raw hex transaction.
if !strings.HasPrefix(lastLine, "Raw tx: 0x") {
t.Fatalf("last output line = %q, want prefix \"Raw tx: 0x\"", lastLine)
}
// Verify the hex payload round-trips back to the input.
rawHex := strings.TrimPrefix(lastLine, "Raw tx: ")
if rawHex != txHex {
t.Fatalf("raw hex mismatch:\n got %s\n want %s", rawHex, txHex)
}
}