Skip to content

Commit 44d39b6

Browse files
committed
Add support for eth_simulateV1:
- [unrelated] Open up the typing for ``StateOverride`` type to match ``to`` and ``from`` keys on ``TxParams``. Accept ``Union[str, Address, ChecksumAddress]`` for these similar types. - Add sync and async ``eth_simulateV1`` RPC methods with integration tests against geth. - Add related types and formatters for ``eth_simulateV1`` RPC method.
1 parent ed60a8a commit 44d39b6

File tree

6 files changed

+274
-23
lines changed

6 files changed

+274
-23
lines changed

web3/_utils/method_formatters.py

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
BlockIdentifier,
102102
Formatters,
103103
RPCEndpoint,
104+
SimulateV1Payload,
104105
StateOverrideParams,
105106
TReturn,
106107
TxParams,
@@ -309,7 +310,32 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
309310

310311
receipt_formatter = type_aware_apply_formatters_to_dict(RECEIPT_FORMATTERS)
311312

312-
BLOCK_FORMATTERS = {
313+
BLOCK_REQUEST_FORMATTERS = {
314+
"baseFeePerGas": to_hex_if_integer,
315+
"extraData": to_hex_if_bytes,
316+
"gasLimit": to_hex_if_integer,
317+
"gasUsed": to_hex_if_integer,
318+
"size": to_hex_if_integer,
319+
"timestamp": to_hex_if_integer,
320+
"hash": to_hex_if_bytes,
321+
"logsBloom": to_hex_if_bytes,
322+
"miner": to_checksum_address,
323+
"mixHash": to_hex_if_bytes,
324+
"nonce": to_hex_if_bytes,
325+
"number": to_hex_if_integer,
326+
"parentHash": to_hex_if_bytes,
327+
"sha3Uncles": to_hex_if_bytes,
328+
"difficulty": to_hex_if_integer,
329+
"receiptsRoot": to_hex_if_bytes,
330+
"stateRoot": to_hex_if_bytes,
331+
"totalDifficulty": to_hex_if_integer,
332+
"transactionsRoot": to_hex_if_bytes,
333+
"withdrawalsRoot": to_hex_if_bytes,
334+
"parentBeaconBlockRoot": to_hex_if_bytes,
335+
}
336+
block_request_formatter = type_aware_apply_formatters_to_dict(BLOCK_REQUEST_FORMATTERS)
337+
338+
BLOCK_RESULT_FORMATTERS = {
313339
"baseFeePerGas": to_integer_if_hex,
314340
"extraData": apply_formatter_if(is_not_null, to_hexbytes(32, variable_length=True)),
315341
"gasLimit": to_integer_if_hex,
@@ -349,9 +375,7 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
349375
"excessBlobGas": to_integer_if_hex,
350376
"parentBeaconBlockRoot": apply_formatter_if(is_not_null, to_hexbytes(32)),
351377
}
352-
353-
354-
block_formatter = type_aware_apply_formatters_to_dict(BLOCK_FORMATTERS)
378+
block_result_formatter = type_aware_apply_formatters_to_dict(BLOCK_RESULT_FORMATTERS)
355379

356380

357381
SYNCING_FORMATTERS = {
@@ -442,6 +466,22 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
442466
)
443467
)
444468

469+
TRANSACTION_REQUEST_FORMATTER = {
470+
"from": to_checksum_address,
471+
"to": apply_formatter_if(is_address, to_checksum_address),
472+
"gas": to_hex_if_integer,
473+
"gasPrice": to_hex_if_integer,
474+
"value": to_hex_if_integer,
475+
"data": to_hex_if_bytes,
476+
"nonce": to_hex_if_integer,
477+
"maxFeePerGas": to_hex_if_integer,
478+
"maxPriorityFeePerGas": to_hex_if_integer,
479+
"chainId": to_hex_if_integer,
480+
}
481+
transaction_request_formatter = type_aware_apply_formatters_to_dict(
482+
TRANSACTION_REQUEST_FORMATTER
483+
)
484+
445485
ACCESS_LIST_REQUEST_FORMATTER = type_aware_apply_formatters_to_dict(
446486
{
447487
"accessList": apply_formatter_if(
@@ -472,11 +512,15 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
472512
]
473513
)
474514

475-
CALL_OVERRIDE_FORMATTERS = {
515+
STATE_OVERRIDE_FORMATTERS = {
476516
"balance": to_hex_if_integer,
477517
"nonce": to_hex_if_integer,
478518
"code": to_hex_if_bytes,
479519
}
520+
state_override_formatter = type_aware_apply_formatters_to_dict(
521+
STATE_OVERRIDE_FORMATTERS
522+
)
523+
480524
call_with_override: Callable[
481525
[Tuple[TxParams, BlockIdentifier, StateOverrideParams]],
482526
Tuple[Dict[str, Any], int, Dict[str, Any]],
@@ -486,29 +530,24 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
486530
to_hex_if_integer,
487531
lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
488532
to_checksum_address,
489-
type_aware_apply_formatters_to_dict(CALL_OVERRIDE_FORMATTERS),
533+
state_override_formatter,
490534
val,
491535
),
492536
]
493537
)
494538

495539

496-
estimate_gas_without_block_id: Callable[[Dict[str, Any]], Dict[str, Any]]
497-
estimate_gas_without_block_id = apply_formatter_at_index(transaction_param_formatter, 0)
540+
estimate_gas_without_block_id: Callable[
541+
[Dict[str, Any]], Dict[str, Any]
542+
] = apply_formatter_at_index(transaction_param_formatter, 0)
498543
estimate_gas_with_block_id: Callable[
499544
[Tuple[Dict[str, Any], BlockIdentifier]], Tuple[Dict[str, Any], int]
500-
]
501-
estimate_gas_with_block_id = apply_formatters_to_sequence(
545+
] = apply_formatters_to_sequence(
502546
[
503547
transaction_param_formatter,
504548
to_hex_if_integer,
505549
]
506550
)
507-
ESTIMATE_GAS_OVERRIDE_FORMATTERS = {
508-
"balance": to_hex_if_integer,
509-
"nonce": to_hex_if_integer,
510-
"code": to_hex_if_bytes,
511-
}
512551
estimate_gas_with_override: Callable[
513552
[Tuple[Dict[str, Any], BlockIdentifier, StateOverrideParams]],
514553
Tuple[Dict[str, Any], int, Dict[str, Any]],
@@ -518,12 +557,71 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
518557
to_hex_if_integer,
519558
lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
520559
to_checksum_address,
521-
type_aware_apply_formatters_to_dict(ESTIMATE_GAS_OVERRIDE_FORMATTERS),
560+
state_override_formatter,
522561
val,
523562
),
524563
]
525564
)
526565

566+
# -- eth_simulateV1 -- #
567+
568+
block_state_calls_formatter: Callable[
569+
[Dict[str, Any]], Dict[str, Any]
570+
] = apply_formatter_to_array(
571+
apply_formatters_to_dict(
572+
{
573+
"blockOverrides": block_request_formatter,
574+
"stateOverrides": (
575+
lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
576+
to_checksum_address,
577+
state_override_formatter,
578+
val,
579+
)
580+
),
581+
"calls": apply_formatter_to_array(transaction_request_formatter),
582+
},
583+
),
584+
)
585+
586+
simulate_v1_request_formatter: Callable[
587+
[Tuple[Dict[str, Any], bool, bool], BlockIdentifier],
588+
Tuple[SimulateV1Payload, BlockIdentifier],
589+
] = apply_formatters_to_sequence(
590+
[
591+
# payload
592+
apply_formatters_to_dict(
593+
{
594+
"blockStateCalls": block_state_calls_formatter,
595+
},
596+
),
597+
# block_identifier
598+
to_hex_if_integer,
599+
]
600+
)
601+
602+
block_result_formatters_copy = BLOCK_RESULT_FORMATTERS.copy()
603+
block_result_formatters_copy.update(
604+
{
605+
"calls": apply_list_to_array_formatter(
606+
type_aware_apply_formatters_to_dict(
607+
{
608+
"returnData": HexBytes,
609+
"logs": apply_list_to_array_formatter(log_entry_formatter),
610+
"gasUsed": to_integer_if_hex,
611+
"status": to_integer_if_hex,
612+
}
613+
)
614+
)
615+
}
616+
)
617+
simulate_v1_result_formatter = apply_formatter_if(
618+
is_not_null,
619+
apply_list_to_array_formatter(
620+
type_aware_apply_formatters_to_dict(block_result_formatters_copy)
621+
),
622+
)
623+
624+
527625
SIGNED_TX_FORMATTER = {
528626
"raw": HexBytes,
529627
"tx": transaction_result_formatter,
@@ -587,6 +685,7 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
587685
(is_length(3), call_with_override),
588686
)
589687
),
688+
RPC.eth_simulateV1: simulate_v1_request_formatter,
590689
RPC.eth_createAccessList: apply_formatter_at_index(transaction_param_formatter, 0),
591690
RPC.eth_estimateGas: apply_one_of_formatters(
592691
(
@@ -610,6 +709,7 @@ def storage_key_to_hexstr(value: Union[bytes, int, str]) -> HexStr:
610709
}
611710

612711
# --- Result Formatters --- #
712+
613713
# -- debug -- #
614714
DEBUG_CALLTRACE_LOG_ENTRY_FORMATTERS = apply_formatter_if(
615715
is_not_null,
@@ -795,11 +895,11 @@ def subscription_formatter(value: Any) -> Union[HexBytes, HexStr, Dict[str, Any]
795895
# handle dict subscription responses
796896
if either_set_is_a_subset(
797897
result_key_set,
798-
set(BLOCK_FORMATTERS.keys()),
898+
set(BLOCK_RESULT_FORMATTERS.keys()),
799899
percentage=90,
800900
):
801901
# block format, newHeads
802-
result_formatter = block_formatter
902+
result_formatter = block_result_formatter
803903

804904
elif either_set_is_a_subset(
805905
result_key_set, set(LOG_ENTRY_FORMATTERS.keys()), percentage=75
@@ -846,8 +946,8 @@ def subscription_formatter(value: Any) -> Union[HexBytes, HexStr, Dict[str, Any]
846946
RPC.eth_maxPriorityFeePerGas: to_integer_if_hex,
847947
RPC.eth_gasPrice: to_integer_if_hex,
848948
RPC.eth_getBalance: to_integer_if_hex,
849-
RPC.eth_getBlockByHash: apply_formatter_if(is_not_null, block_formatter),
850-
RPC.eth_getBlockByNumber: apply_formatter_if(is_not_null, block_formatter),
949+
RPC.eth_getBlockByHash: apply_formatter_if(is_not_null, block_result_formatter),
950+
RPC.eth_getBlockByNumber: apply_formatter_if(is_not_null, block_result_formatter),
851951
RPC.eth_getBlockReceipts: apply_formatter_to_array(receipt_formatter),
852952
RPC.eth_getBlockTransactionCountByHash: to_integer_if_hex,
853953
RPC.eth_getBlockTransactionCountByNumber: to_integer_if_hex,
@@ -887,6 +987,7 @@ def subscription_formatter(value: Any) -> Union[HexBytes, HexStr, Dict[str, Any]
887987
RPC.eth_sign: HexBytes,
888988
RPC.eth_signTransaction: apply_formatter_if(is_not_null, signed_tx_formatter),
889989
RPC.eth_signTypedData: HexBytes,
990+
RPC.eth_simulateV1: simulate_v1_result_formatter,
890991
RPC.eth_syncing: apply_formatter_if(is_not_false, syncing_formatter),
891992
# Transaction Pool
892993
RPC.txpool_content: transaction_pool_content_formatter,

web3/_utils/module_testing/eth_module.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,52 @@ async def test_eth_call_continuous_offchain_lookup_raises_with_too_many_requests
16901690
with pytest.raises(TooManyRequests, match="Too many CCIP read redirects"):
16911691
await async_offchain_lookup_contract.caller().continuousOffchainLookup() # noqa: E501 type: ignore
16921692

1693+
@pytest.mark.asyncio
1694+
async def test_eth_simulateV1(self, async_w3: "AsyncWeb3") -> None:
1695+
simulate_result = await async_w3.eth.simulateV1(
1696+
{
1697+
"blockStateCalls": [
1698+
{
1699+
"blockOverrides": {
1700+
"baseFeePerGas": Wei(10),
1701+
},
1702+
"stateOverrides": {
1703+
"0xc100000000000000000000000000000000000000": {
1704+
"balance": Wei(500000000),
1705+
}
1706+
},
1707+
"calls": [
1708+
{
1709+
"from": "0xc100000000000000000000000000000000000000",
1710+
"to": "0xc100000000000000000000000000000000000000",
1711+
"maxFeePerGas": Wei(10),
1712+
"maxPriorityFeePerGas": Wei(10),
1713+
}
1714+
],
1715+
}
1716+
],
1717+
"validation": True,
1718+
"traceTransfers": True,
1719+
},
1720+
"latest",
1721+
)
1722+
1723+
assert len(simulate_result) == 1
1724+
1725+
result = simulate_result[0]
1726+
assert result.get("baseFeePerGas") == 10
1727+
1728+
calls_result = result.get("calls")
1729+
assert calls_result is not None
1730+
assert len(calls_result) == 1
1731+
call_entry = calls_result[0]
1732+
1733+
assert all(
1734+
key in call_entry for key in ("returnData", "logs", "gasUsed", "status")
1735+
)
1736+
assert call_entry["status"] == 1
1737+
assert call_entry["gasUsed"] == int("0x5208", 16)
1738+
16931739
@pytest.mark.asyncio
16941740
async def test_async_eth_chain_id(self, async_w3: "AsyncWeb3") -> None:
16951741
chain_id = await async_w3.eth.chain_id
@@ -3821,7 +3867,7 @@ def test_eth_call_with_override_param_type_check(
38213867
math_contract: "Contract",
38223868
params: StateOverrideParams,
38233869
) -> None:
3824-
txn_params: TxParams = {"from": w3.eth.accounts[0]}
3870+
txn_params: TxParams = {"from": w3.eth.accounts[0], "to": math_contract.address}
38253871

38263872
# assert does not raise
38273873
w3.eth.call(txn_params, "latest", {math_contract.address: params})
@@ -3914,6 +3960,51 @@ def test_eth_call_custom_error_revert_without_msg(
39143960
w3.eth.call(txn_params)
39153961
assert excinfo.value.data == data
39163962

3963+
def test_eth_simulateV1(self, w3: "Web3") -> None:
3964+
simulate_result = w3.eth.simulateV1(
3965+
{
3966+
"blockStateCalls": [
3967+
{
3968+
"blockOverrides": {
3969+
"baseFeePerGas": Wei(10),
3970+
},
3971+
"stateOverrides": {
3972+
"0xc100000000000000000000000000000000000000": {
3973+
"balance": Wei(500000000),
3974+
}
3975+
},
3976+
"calls": [
3977+
{
3978+
"from": "0xc100000000000000000000000000000000000000",
3979+
"to": "0xc100000000000000000000000000000000000000",
3980+
"maxFeePerGas": Wei(10),
3981+
"maxPriorityFeePerGas": Wei(10),
3982+
}
3983+
],
3984+
}
3985+
],
3986+
"validation": True,
3987+
"traceTransfers": True,
3988+
},
3989+
"latest",
3990+
)
3991+
3992+
assert len(simulate_result) == 1
3993+
3994+
result = simulate_result[0]
3995+
assert result.get("baseFeePerGas") == 10
3996+
3997+
calls_result = result.get("calls")
3998+
assert calls_result is not None
3999+
assert len(calls_result) == 1
4000+
call_entry = calls_result[0]
4001+
4002+
assert all(
4003+
key in call_entry for key in ("returnData", "logs", "gasUsed", "status")
4004+
)
4005+
assert call_entry["status"] == 1
4006+
assert call_entry["gasUsed"] == int("0x5208", 16)
4007+
39174008
@pytest.mark.parametrize(
39184009
"panic_error,params",
39194010
(

web3/_utils/rpc_abi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class RPC:
5151
eth_blobBaseFee = RPCEndpoint("eth_blobBaseFee")
5252
eth_blockNumber = RPCEndpoint("eth_blockNumber")
5353
eth_call = RPCEndpoint("eth_call")
54+
eth_simulateV1 = RPCEndpoint("eth_simulateV1")
5455
eth_createAccessList = RPCEndpoint("eth_createAccessList")
5556
eth_chainId = RPCEndpoint("eth_chainId")
5657
eth_estimateGas = RPCEndpoint("eth_estimateGas")

0 commit comments

Comments
 (0)