ethclient/gethclient: callTracer methods (#31510)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

Added methods `TraceCallWithCallTracer` and `TraceTransactionWithCallTracer`.

Fixes #28182

---------

Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
This commit is contained in:
Ragnar 2026-02-10 18:54:37 +02:00 committed by GitHub
parent 4d4883731e
commit 15a9e92bbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 349 additions and 0 deletions

View file

@ -0,0 +1,104 @@
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
package gethclient
import (
"encoding/json"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
var _ = (*callFrameMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (c CallFrame) MarshalJSON() ([]byte, error) {
type CallFrame0 struct {
Type string `json:"type"`
From common.Address `json:"from"`
Gas hexutil.Uint64 `json:"gas"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
To *common.Address `json:"to,omitempty"`
Input hexutil.Bytes `json:"input"`
Output hexutil.Bytes `json:"output,omitempty"`
Error string `json:"error,omitempty"`
RevertReason string `json:"revertReason,omitempty"`
Calls []CallFrame `json:"calls,omitempty"`
Logs []CallLog `json:"logs,omitempty"`
Value *hexutil.Big `json:"value,omitempty"`
}
var enc CallFrame0
enc.Type = c.Type
enc.From = c.From
enc.Gas = hexutil.Uint64(c.Gas)
enc.GasUsed = hexutil.Uint64(c.GasUsed)
enc.To = c.To
enc.Input = c.Input
enc.Output = c.Output
enc.Error = c.Error
enc.RevertReason = c.RevertReason
enc.Calls = c.Calls
enc.Logs = c.Logs
enc.Value = (*hexutil.Big)(c.Value)
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (c *CallFrame) UnmarshalJSON(input []byte) error {
type CallFrame0 struct {
Type *string `json:"type"`
From *common.Address `json:"from"`
Gas *hexutil.Uint64 `json:"gas"`
GasUsed *hexutil.Uint64 `json:"gasUsed"`
To *common.Address `json:"to,omitempty"`
Input *hexutil.Bytes `json:"input"`
Output *hexutil.Bytes `json:"output,omitempty"`
Error *string `json:"error,omitempty"`
RevertReason *string `json:"revertReason,omitempty"`
Calls []CallFrame `json:"calls,omitempty"`
Logs []CallLog `json:"logs,omitempty"`
Value *hexutil.Big `json:"value,omitempty"`
}
var dec CallFrame0
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
if dec.Type != nil {
c.Type = *dec.Type
}
if dec.From != nil {
c.From = *dec.From
}
if dec.Gas != nil {
c.Gas = uint64(*dec.Gas)
}
if dec.GasUsed != nil {
c.GasUsed = uint64(*dec.GasUsed)
}
if dec.To != nil {
c.To = dec.To
}
if dec.Input != nil {
c.Input = *dec.Input
}
if dec.Output != nil {
c.Output = *dec.Output
}
if dec.Error != nil {
c.Error = *dec.Error
}
if dec.RevertReason != nil {
c.RevertReason = *dec.RevertReason
}
if dec.Calls != nil {
c.Calls = dec.Calls
}
if dec.Logs != nil {
c.Logs = dec.Logs
}
if dec.Value != nil {
c.Value = (*big.Int)(dec.Value)
}
return nil
}

View file

@ -0,0 +1,61 @@
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
package gethclient
import (
"encoding/json"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
var _ = (*callLogMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (c CallLog) MarshalJSON() ([]byte, error) {
type CallLog struct {
Address common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data hexutil.Bytes `json:"data"`
Index hexutil.Uint `json:"index"`
Position hexutil.Uint `json:"position"`
}
var enc CallLog
enc.Address = c.Address
enc.Topics = c.Topics
enc.Data = c.Data
enc.Index = hexutil.Uint(c.Index)
enc.Position = hexutil.Uint(c.Position)
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (c *CallLog) UnmarshalJSON(input []byte) error {
type CallLog struct {
Address *common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data *hexutil.Bytes `json:"data"`
Index *hexutil.Uint `json:"index"`
Position *hexutil.Uint `json:"position"`
}
var dec CallLog
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
if dec.Address != nil {
c.Address = *dec.Address
}
if dec.Topics != nil {
c.Topics = dec.Topics
}
if dec.Data != nil {
c.Data = *dec.Data
}
if dec.Index != nil {
c.Index = uint(*dec.Index)
}
if dec.Position != nil {
c.Position = uint(*dec.Position)
}
return nil
}

View file

@ -19,10 +19,12 @@ package gethclient
import (
"context"
"encoding/json"
"fmt"
"math/big"
"runtime"
"runtime/debug"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
@ -229,6 +231,124 @@ func (ec *Client) TraceBlock(ctx context.Context, hash common.Hash, config *trac
return result, nil
}
// CallTracerConfig configures the call tracer for
// TraceTransactionWithCallTracer and TraceCallWithCallTracer.
type CallTracerConfig struct {
// OnlyTopCall, when true, limits tracing to the main (top-level) call only.
OnlyTopCall bool
// WithLog, when true, includes log emissions in the trace output.
WithLog bool
// Timeout is the maximum duration the tracer may run.
// Zero means the server default (5s).
Timeout time.Duration
}
//go:generate go run github.com/fjl/gencodec -type CallLog -field-override callLogMarshaling -out gen_calllog_json.go
// CallLog represents a log emitted during a traced call.
type CallLog struct {
Address common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data []byte `json:"data"`
Index uint `json:"index"`
Position uint `json:"position"`
}
type callLogMarshaling struct {
Data hexutil.Bytes
Index hexutil.Uint
Position hexutil.Uint
}
//go:generate go run github.com/fjl/gencodec -type CallFrame -field-override callFrameMarshaling -out gen_callframe_json.go
// CallFrame contains the result of a call tracer run.
type CallFrame struct {
Type string `json:"type"`
From common.Address `json:"from"`
Gas uint64 `json:"gas"`
GasUsed uint64 `json:"gasUsed"`
To *common.Address `json:"to,omitempty"`
Input []byte `json:"input"`
Output []byte `json:"output,omitempty"`
Error string `json:"error,omitempty"`
RevertReason string `json:"revertReason,omitempty"`
Calls []CallFrame `json:"calls,omitempty"`
Logs []CallLog `json:"logs,omitempty"`
Value *big.Int `json:"value,omitempty"`
}
type callFrameMarshaling struct {
Gas hexutil.Uint64
GasUsed hexutil.Uint64
Input hexutil.Bytes
Output hexutil.Bytes
Value *hexutil.Big
}
// TraceTransactionWithCallTracer traces a transaction with the call tracer
// and returns a typed CallFrame. If config is nil, defaults are used.
func (ec *Client) TraceTransactionWithCallTracer(ctx context.Context, txHash common.Hash, config *CallTracerConfig) (*CallFrame, error) {
var result CallFrame
err := ec.c.CallContext(ctx, &result, "debug_traceTransaction", txHash, callTracerConfig(config))
if err != nil {
return nil, err
}
return &result, nil
}
// TraceCallWithCallTracer executes a call with the call tracer and returns
// a typed CallFrame. blockNrOrHash selects the block context for the call.
// overrides specifies state overrides (nil for none), blockOverrides specifies
// block header overrides (nil for none), and config configures the tracer
// (nil for defaults).
func (ec *Client) TraceCallWithCallTracer(ctx context.Context, msg ethereum.CallMsg, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]OverrideAccount, blockOverrides *BlockOverrides, config *CallTracerConfig) (*CallFrame, error) {
var result CallFrame
err := ec.c.CallContext(ctx, &result, "debug_traceCall", toCallArg(msg), blockNrOrHash, callTraceCallConfig(config, overrides, blockOverrides))
if err != nil {
return nil, err
}
return &result, nil
}
// callTracerConfig converts a CallTracerConfig to the wire-format TraceConfig.
func callTracerConfig(config *CallTracerConfig) *tracers.TraceConfig {
tracer := "callTracer"
tc := &tracers.TraceConfig{Tracer: &tracer}
if config != nil {
if config.OnlyTopCall || config.WithLog {
cfg, _ := json.Marshal(struct {
OnlyTopCall bool `json:"onlyTopCall"`
WithLog bool `json:"withLog"`
}{config.OnlyTopCall, config.WithLog})
tc.TracerConfig = cfg
}
if config.Timeout != 0 {
s := config.Timeout.String()
tc.Timeout = &s
}
}
return tc
}
// callTraceCallConfig builds the wire-format TraceCallConfig for debug_traceCall,
// bundling tracer settings with optional state and block overrides.
func callTraceCallConfig(config *CallTracerConfig, overrides map[common.Address]OverrideAccount, blockOverrides *BlockOverrides) interface{} {
tc := callTracerConfig(config)
// debug_traceCall expects a single config object that includes both
// tracer settings and any state/block overrides.
type traceCallConfig struct {
*tracers.TraceConfig
StateOverrides map[common.Address]OverrideAccount `json:"stateOverrides,omitempty"`
BlockOverrides *BlockOverrides `json:"blockOverrides,omitempty"`
}
return &traceCallConfig{
TraceConfig: tc,
StateOverrides: overrides,
BlockOverrides: blockOverrides,
}
}
func toBlockNumArg(number *big.Int) string {
if number == nil {
return "latest"

View file

@ -34,6 +34,7 @@ import (
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/eth/tracers"
_ "github.com/ethereum/go-ethereum/eth/tracers/native"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
@ -161,6 +162,12 @@ func TestGethClient(t *testing.T) {
}, {
"TestCallContractWithBlockOverrides",
func(t *testing.T) { testCallContractWithBlockOverrides(t, client) },
}, {
"TestTraceTransactionWithCallTracer",
func(t *testing.T) { testTraceTransactionWithCallTracer(t, client, txHashes) },
}, {
"TestTraceCallWithCallTracer",
func(t *testing.T) { testTraceCallWithCallTracer(t, client) },
},
// The testaccesslist is a bit time-sensitive: the newTestBackend imports
// one block. The `testAccessList` fails if the miner has not yet created a
@ -620,3 +627,60 @@ func testCallContractWithBlockOverrides(t *testing.T, client *rpc.Client) {
t.Fatalf("unexpected result: %x", res)
}
}
func testTraceTransactionWithCallTracer(t *testing.T, client *rpc.Client, txHashes []common.Hash) {
ec := New(client)
for _, txHash := range txHashes {
// With nil config (defaults).
result, err := ec.TraceTransactionWithCallTracer(context.Background(), txHash, nil)
if err != nil {
t.Fatalf("nil config: %v", err)
}
if result.Type != "CALL" {
t.Fatalf("unexpected type: %s", result.Type)
}
if result.From == (common.Address{}) {
t.Fatal("from is zero")
}
if result.Gas == 0 {
t.Fatal("gas is zero")
}
// With explicit config.
result, err = ec.TraceTransactionWithCallTracer(context.Background(), txHash,
&CallTracerConfig{},
)
if err != nil {
t.Fatalf("explicit config: %v", err)
}
if result.Type != "CALL" {
t.Fatalf("unexpected type: %s", result.Type)
}
}
}
func testTraceCallWithCallTracer(t *testing.T, client *rpc.Client) {
ec := New(client)
msg := ethereum.CallMsg{
From: testAddr,
To: &common.Address{},
Gas: 21000,
GasPrice: big.NewInt(1000000000),
Value: big.NewInt(1),
}
result, err := ec.TraceCallWithCallTracer(context.Background(), msg,
rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), nil, nil, nil,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Type != "CALL" {
t.Fatalf("unexpected type: %s", result.Type)
}
if result.From == (common.Address{}) {
t.Fatal("from is zero")
}
if result.Gas == 0 {
t.Fatal("gas is zero")
}
}