diff --git a/ethrpc/ethrpc.go b/ethrpc/ethrpc.go index cbc9ff26..c8cb8547 100644 --- a/ethrpc/ethrpc.go +++ b/ethrpc/ethrpc.go @@ -488,6 +488,12 @@ func (p *Provider) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint6 return result, err } +func (p *Provider) SimulateV1(ctx context.Context, payload EthSimulatePayload) ([]*SimulatedBlock, error) { + var result []*SimulatedBlock + _, err := p.Do(ctx, SimulateV1(payload).Strict(p.strictness).Into(&result)) + return result, err +} + func (p *Provider) DebugTraceBlockByNumber(ctx context.Context, blockNum *big.Int) ([]*TransactionDebugTrace, error) { var result []*TransactionDebugTrace _, err := p.Do(ctx, DebugTraceBlockByNumber(blockNum).Into(&result)) diff --git a/ethrpc/ethrpc_test.go b/ethrpc/ethrpc_test.go index f2dd32c2..2ee0b573 100644 --- a/ethrpc/ethrpc_test.go +++ b/ethrpc/ethrpc_test.go @@ -11,6 +11,7 @@ import ( "github.com/0xsequence/ethkit/ethtest" "github.com/0xsequence/ethkit/go-ethereum" "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/ethkit/go-ethereum/common/hexutil" "github.com/0xsequence/ethkit/go-ethereum/core/types" "github.com/goware/logger" "github.com/stretchr/testify/assert" @@ -296,6 +297,50 @@ func TestDebugTraceTransaction(t *testing.T) { require.NotEmpty(t, payload) }*/ +func TestSimulateV1(t *testing.T) { + p, err := ethrpc.NewProvider("https://eth.llamarpc.com") // Llama RPC supports this method + require.NoError(t, err) + walletAddr := ethtest.DummyAddr() + recipientAddr := ethtest.DummyAddr() + value := big.NewInt(1000000000000000000) // 1 ETH + + ctx := context.Background() + payload := ethrpc.EthSimulatePayload{ + BlockStateCalls: []ethrpc.BlockStateCall{ + { + Calls: []ethrpc.GenericCallTransaction{ + { + From: walletAddr, + To: recipientAddr, + Value: value.String(), + Data: []byte{}, + }, + }, + StateOverrides: map[string]ethrpc.StateOverride{ + walletAddr.Hex(): { + Balance: (*hexutil.Big)(value), + }, + }, + }, + }, + } + blocks, err := p.SimulateV1(ctx, payload) + require.NoError(t, err) + require.NotEmpty(t, blocks) + + for _, block := range blocks { + require.NotNil(t, block) + require.NotNil(t, block.Calls) + require.Greater(t, block.GasLimit, hexutil.Uint64(0)) + require.Equal(t, hexutil.Uint64(21000), block.GasUsed) + require.Equal(t, 1, len(block.Transactions)) + require.Equal(t, 1, len(block.Calls)) + require.Equal(t, "0x1", block.Calls[0].Status) + require.Nil(t, block.Calls[0].Error) + require.Equal(t, hexutil.Uint64(21000), block.Calls[0].GasUsed) + } +} + // func TestJWTAuth(t *testing.T) { // p, err := ethrpc.NewProvider("https://dev-nodes.sequence.app/polygon", ethrpc.WithJWTAuthorization("xx")) // require.NoError(t, err) diff --git a/ethrpc/methods.go b/ethrpc/methods.go index ac28d504..c84031f0 100644 --- a/ethrpc/methods.go +++ b/ethrpc/methods.go @@ -419,6 +419,100 @@ func EstimateGas(msg ethereum.CallMsg) CallBuilder[uint64] { } } +type EthSimulatePayload struct { + BlockStateCalls []BlockStateCall `json:"blockStateCalls"` + TraceTransfers bool `json:"traceTransfers,omitempty"` + Validation bool `json:"validation,omitempty"` + ReturnFullTransactions bool `json:"returnFullTransactions,omitempty"` +} + +type BlockStateCall struct { + BlockOverrides interface{} `json:"blockOverrides,omitempty"` + StateOverrides map[string]StateOverride `json:"stateOverrides,omitempty"` + Calls []GenericCallTransaction `json:"calls"` +} + +type StateOverride struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Nonce *hexutil.Uint64 `json:"nonce,omitempty"` + Code *hexutil.Bytes `json:"code,omitempty"` + Storage map[common.Hash]common.Hash `json:"state,omitempty"` +} + +type GenericCallTransaction struct { + From common.Address `json:"from"` + To common.Address `json:"to"` + Gas string `json:"gas,omitempty"` + GasPrice string `json:"gasPrice,omitempty"` + MaxFeePerGas string `json:"maxFeePerGas,omitempty"` + Value string `json:"value,omitempty"` + Data hexutil.Bytes `json:"data,omitempty"` +} + +type SimulatedCall struct { + Status string `json:"status"` // "0x1" for success, "0x0" for failure + ReturnData hexutil.Bytes `json:"returnData"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + Error *SimulatedError `json:"error,omitempty"` + Logs []CallResultLog `json:"logs,omitempty"` +} + +type SimulatedError struct { + Code uint64 `json:"code"` + Message string `json:"message"` + Data hexutil.Bytes `json:"data"` +} + +type CallResultLog struct { + LogIndex hexutil.Uint64 `json:"logIndex"` + BlockHash common.Hash `json:"blockHash"` + BlockNumber hexutil.Uint64 `json:"blockNumber"` + TransactionHash common.Hash `json:"transactionHash"` + TransactionIndex hexutil.Uint64 `json:"transactionIndex"` + Address common.Address `json:"address"` + Data hexutil.Bytes `json:"data"` + Topics []common.Hash `json:"topics"` + Removed bool `json:"removed"` +} + +type SimulatedBlock struct { + BaseFeePerGas hexutil.Big `json:"baseFeePerGas"` + Difficulty hexutil.Big `json:"difficulty"` + ExtraData hexutil.Bytes `json:"extraData"` + GasLimit hexutil.Uint64 `json:"gasLimit"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + Hash common.Hash `json:"hash"` + LogsBloom hexutil.Bytes `json:"logsBloom"` + Miner common.Address `json:"miner"` + MixHash common.Hash `json:"mixHash"` + Nonce hexutil.Uint64 `json:"nonce"` + Number hexutil.Big `json:"number"` + ParentHash common.Hash `json:"parentHash"` + ReceiptsRoot common.Hash `json:"receiptsRoot"` + Sha3Uncles common.Hash `json:"sha3Uncles"` + Size hexutil.Uint64 `json:"size"` + StateRoot common.Hash `json:"stateRoot"` + Timestamp hexutil.Uint64 `json:"timestamp"` + TotalDifficulty hexutil.Big `json:"totalDifficulty"` + Transactions []string `json:"transactions"` + Calls []SimulatedCall `json:"calls"` +} + +func SimulateV1(payload EthSimulatePayload) CallBuilder[[]*SimulatedBlock] { + return CallBuilder[[]*SimulatedBlock]{ + method: "eth_simulateV1", + params: []any{payload}, + intoFn: func(raw json.RawMessage, ret *[]*SimulatedBlock, strictness StrictnessLevel) error { + var blocks []*SimulatedBlock + if err := json.Unmarshal(raw, &blocks); err != nil { + return fmt.Errorf("eth_simulateV1 unmarshal failed: %w", err) + } + *ret = blocks + return nil + }, + } +} + type DebugTracer string const (