Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions ethclient/ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,3 +819,89 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
StateIndexRemaining: uint64(p.StateIndexRemaining),
}
}

// SimulateOptions represents the options for eth_simulateV1.
type SimulateOptions struct {
BlockStateCalls []SimulateBlock `json:"blockStateCalls"`
TraceTransfers bool `json:"traceTransfers"`
Validation bool `json:"validation"`
ReturnFullTransactions bool `json:"returnFullTransactions"`
}

// SimulateBlock represents a batch of calls to be simulated.
type SimulateBlock struct {
BlockOverrides *ethereum.BlockOverrides `json:"blockOverrides,omitempty"`
StateOverrides map[common.Address]ethereum.OverrideAccount `json:"stateOverrides,omitempty"`
Calls []ethereum.CallMsg `json:"calls"`
}

// MarshalJSON implements json.Marshaler for SimulateBlock.
func (s SimulateBlock) MarshalJSON() ([]byte, error) {
type Alias struct {
BlockOverrides *ethereum.BlockOverrides `json:"blockOverrides,omitempty"`
StateOverrides map[common.Address]ethereum.OverrideAccount `json:"stateOverrides,omitempty"`
Calls []interface{} `json:"calls"`
}
calls := make([]interface{}, len(s.Calls))
for i, call := range s.Calls {
calls[i] = toCallArg(call)
}
return json.Marshal(Alias{
BlockOverrides: s.BlockOverrides,
StateOverrides: s.StateOverrides,
Calls: calls,
})
}

//go:generate go run github.com/fjl/gencodec -type SimulateCallResult -field-override simulateCallResultMarshaling -out gen_simulate_call_result.go

// SimulateCallResult is the result of a simulated call.
type SimulateCallResult struct {
ReturnValue []byte `json:"returnData"`
Logs []*types.Log `json:"logs"`
GasUsed uint64 `json:"gasUsed"`
Status uint64 `json:"status"`
Error *CallError `json:"error,omitempty"`
}

type simulateCallResultMarshaling struct {
ReturnValue hexutil.Bytes
GasUsed hexutil.Uint64
Status hexutil.Uint64
}

// CallError represents an error from a simulated call.
type CallError struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"`
}

//go:generate go run github.com/fjl/gencodec -type SimulateBlockResult -field-override simulateBlockResultMarshaling -out gen_simulate_block_result.go

// SimulateBlockResult represents the result of a simulated block.
type SimulateBlockResult struct {
Number uint64 `json:"number"`
Hash common.Hash `json:"hash"`
Timestamp uint64 `json:"timestamp"`
GasLimit uint64 `json:"gasLimit"`
GasUsed uint64 `json:"gasUsed"`
FeeRecipient common.Address `json:"miner"`
BaseFeePerGas *big.Int `json:"baseFeePerGas,omitempty"`
Calls []SimulateCallResult `json:"calls"`
}

type simulateBlockResultMarshaling struct {
Number hexutil.Uint64
Timestamp hexutil.Uint64
GasLimit hexutil.Uint64
GasUsed hexutil.Uint64
BaseFeePerGas *hexutil.Big
}

// SimulateV1 executes transactions on top of a base state.
func (ec *Client) SimulateV1(ctx context.Context, opts SimulateOptions, blockNrOrHash *rpc.BlockNumberOrHash) ([]SimulateBlockResult, error) {
var result []SimulateBlockResult
err := ec.c.CallContext(ctx, &result, "eth_simulateV1", opts, blockNrOrHash)
return result, err
}
247 changes: 247 additions & 0 deletions ethclient/ethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,250 @@ func ExampleRevertErrorData() {
// revert: 08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a75736572206572726f72
// message: user error
}

func TestSimulateV1(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

// Simple test: transfer ETH from one account to another
from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := big.NewInt(100)
gas := uint64(100000)
maxFeePerGas := new(big.Int).Mul(header.BaseFee, big.NewInt(2))

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
Calls: []ethereum.CallMsg{
{
From: from,
To: &to,
Value: value,
Gas: gas,
GasFeeCap: maxFeePerGas,
},
},
},
},
Validation: true,
}

results, err := client.SimulateV1(ctx, opts, nil)
if err != nil {
t.Fatalf("SimulateV1 failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}

if len(results[0].Calls) != 1 {
t.Fatalf("expected 1 call result, got %d", len(results[0].Calls))
}

// Check that the transaction succeeded
if results[0].Calls[0].Status != 1 {
t.Errorf("expected status 1 (success), got %d", results[0].Calls[0].Status)
}

if results[0].Calls[0].Error != nil {
t.Errorf("expected no error, got %v", results[0].Calls[0].Error)
}
}

func TestSimulateV1WithBlockOverrides(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := big.NewInt(100)
gas := uint64(100000)
maxFeePerGas := new(big.Int).Mul(header.BaseFee, big.NewInt(2))

// Override timestamp only
timestamp := uint64(1234567890)

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
BlockOverrides: &ethereum.BlockOverrides{
Time: timestamp,
},
Calls: []ethereum.CallMsg{
{
From: from,
To: &to,
Value: value,
Gas: gas,
GasFeeCap: maxFeePerGas,
},
},
},
},
Validation: true,
}

results, err := client.SimulateV1(ctx, opts, nil)
if err != nil {
t.Fatalf("SimulateV1 with block overrides failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}

// Verify the timestamp was overridden
if results[0].Timestamp != timestamp {
t.Errorf("expected timestamp %d, got %d", timestamp, results[0].Timestamp)
}
}

func TestSimulateV1WithStateOverrides(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := big.NewInt(1000000000000000000) // 1 ETH
gas := uint64(100000)
maxFeePerGas := new(big.Int).Mul(header.BaseFee, big.NewInt(2))

// Override the balance of the 'from' address
balanceStr := "1000000000000000000000"
balance := new(big.Int)
balance.SetString(balanceStr, 10)

stateOverrides := map[common.Address]ethereum.OverrideAccount{
from: {
Balance: balance,
},
}

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
StateOverrides: stateOverrides,
Calls: []ethereum.CallMsg{
{
From: from,
To: &to,
Value: value,
Gas: gas,
GasFeeCap: maxFeePerGas,
},
},
},
},
Validation: true,
}

results, err := client.SimulateV1(ctx, opts, nil)
if err != nil {
t.Fatalf("SimulateV1 with state overrides failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}

if results[0].Calls[0].Status != 1 {
t.Errorf("expected status 1 (success), got %d", results[0].Calls[0].Status)
}
}

func TestSimulateV1WithBlockNumberOrHash(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := big.NewInt(100)
gas := uint64(100000)
maxFeePerGas := new(big.Int).Mul(header.BaseFee, big.NewInt(2))

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
Calls: []ethereum.CallMsg{
{
From: from,
To: &to,
Value: value,
Gas: gas,
GasFeeCap: maxFeePerGas,
},
},
},
},
Validation: true,
}

// Simulate on the latest block
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
results, err := client.SimulateV1(ctx, opts, &latest)
if err != nil {
t.Fatalf("SimulateV1 with latest block failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}
}
Loading