Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
68 changes: 68 additions & 0 deletions ethclient/ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,3 +819,71 @@ 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,
})
}

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

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

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

// 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 uint64(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