diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst index 870d62006f..3c10f29c33 100644 --- a/docs/web3.eth.rst +++ b/docs/web3.eth.rst @@ -1053,7 +1053,7 @@ The following methods are available on the ``web3.eth`` namespace. }) -.. py:method:: Eth.estimate_gas(transaction, block_identifier=None) +.. py:method:: Eth.estimate_gas(transaction, block_identifier=None, state_override=None) * Delegates to ``eth_estimateGas`` RPC Method @@ -1064,6 +1064,10 @@ The following methods are available on the ``web3.eth`` namespace. The ``transaction`` and ``block_identifier`` parameters are handled in the same manner as the :meth:`~web3.eth.Eth.send_transaction()` method. + The ``state_override`` is useful when there is a chain of transaction calls. + It overrides state so that the gas estimate of a transaction is accurate in + cases where prior calls produce side effects. + .. code-block:: python >>> web3.eth.estimate_gas({'to': '0xd3CdA913deB6f67967B99D67aCDFa1712C293601', 'from':web3.eth.coinbase, 'value': 12345}) diff --git a/newsfragments/3164.feature.rst b/newsfragments/3164.feature.rst new file mode 100644 index 0000000000..769ee69ee7 --- /dev/null +++ b/newsfragments/3164.feature.rst @@ -0,0 +1 @@ +Implement ``state_override`` parameter for ``eth_estimateGas`` method. \ No newline at end of file diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py index 48591b2161..30837acd34 100644 --- a/tests/integration/test_ethereum_tester.py +++ b/tests/integration/test_ethereum_tester.py @@ -304,6 +304,10 @@ class TestEthereumTesterEthModule(EthModuleTest): EthModuleTest.test_eth_call_with_override_param_type_check, TypeError, ) + test_eth_estimate_gas_with_override_param_type_check = not_implemented( + EthModuleTest.test_eth_estimate_gas_with_override_param_type_check, + TypeError, + ) test_eth_create_access_list = not_implemented( EthModuleTest.test_eth_create_access_list, MethodUnavailable, diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py index 56daf3dbe5..b157829e0f 100644 --- a/web3/_utils/method_formatters.py +++ b/web3/_utils/method_formatters.py @@ -455,7 +455,7 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: estimate_gas_without_block_id: Callable[[Dict[str, Any]], Dict[str, Any]] estimate_gas_without_block_id = apply_formatter_at_index(transaction_param_formatter, 0) estimate_gas_with_block_id: Callable[ - [Tuple[Dict[str, Any], Union[str, int]]], Tuple[Dict[str, Any], int] + [Tuple[Dict[str, Any], BlockIdentifier]], Tuple[Dict[str, Any], int] ] estimate_gas_with_block_id = apply_formatters_to_sequence( [ @@ -463,6 +463,25 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: to_hex_if_integer, ] ) +ESTIMATE_GAS_OVERRIDE_FORMATTERS = { + "balance": to_hex_if_integer, + "nonce": to_hex_if_integer, + "code": to_hex_if_bytes, +} +estimate_gas_with_override: Callable[ + [Tuple[Dict[str, Any], BlockIdentifier, CallOverrideParams]], + Tuple[Dict[str, Any], int, Dict[str, Any]], +] = apply_formatters_to_sequence( + [ + transaction_param_formatter, + to_hex_if_integer, + lambda val: type_aware_apply_formatters_to_dict_keys_and_values( + to_checksum_address, + type_aware_apply_formatters_to_dict(ESTIMATE_GAS_OVERRIDE_FORMATTERS), + val, + ), + ] +) SIGNED_TX_FORMATTER = { "raw": HexBytes, @@ -531,6 +550,7 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: ( (is_length(1), estimate_gas_without_block_id), (is_length(2), estimate_gas_with_block_id), + (is_length(3), estimate_gas_with_override), ) ), RPC.eth_sendTransaction: apply_formatter_at_index(transaction_param_formatter, 0), diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py index c366735c8c..1f27549aee 100644 --- a/web3/_utils/module_testing/eth_module.py +++ b/web3/_utils/module_testing/eth_module.py @@ -813,6 +813,39 @@ async def test_eth_estimate_gas( assert is_integer(gas_estimate) assert gas_estimate > 0 + @pytest.mark.asyncio + @pytest.mark.parametrize( + "params", + ( + { + "nonce": 1, # int + "balance": 1, # int + "code": HexStr("0x"), # HexStr + # with state + "state": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")}, + }, + { + "nonce": HexStr("0x1"), # HexStr + "balance": HexStr("0x1"), # HexStr + "code": b"\x00", # bytes + # with stateDiff + "stateDiff": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")}, + }, + ), + ) + async def test_eth_estimate_gas_with_override_param_type_check( + self, + async_w3: "AsyncWeb3", + async_math_contract: "Contract", + params: CallOverrideParams, + ) -> None: + txn_params: TxParams = {"from": await async_w3.eth.coinbase} + + # assert does not raise + await async_w3.eth.estimate_gas( + txn_params, None, {async_math_contract.address: params} + ) + @pytest.mark.asyncio async def test_eth_fee_history(self, async_w3: "AsyncWeb3") -> None: fee_history = await async_w3.eth.fee_history(1, "latest", [50]) @@ -4161,6 +4194,36 @@ def test_eth_estimate_gas_with_block( assert is_integer(gas_estimate) assert gas_estimate > 0 + @pytest.mark.parametrize( + "params", + ( + { + "nonce": 1, # int + "balance": 1, # int + "code": HexStr("0x"), # HexStr + # with state + "state": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")}, + }, + { + "nonce": HexStr("0x1"), # HexStr + "balance": HexStr("0x1"), # HexStr + "code": b"\x00", # bytes + # with stateDiff + "stateDiff": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")}, + }, + ), + ) + def test_eth_estimate_gas_with_override_param_type_check( + self, + w3: "Web3", + math_contract: "Contract", + params: CallOverrideParams, + ) -> None: + txn_params: TxParams = {"from": w3.eth.coinbase} + + # assert does not raise + w3.eth.estimate_gas(txn_params, None, {math_contract.address: params}) + def test_eth_getBlockByHash(self, w3: "Web3", empty_block: BlockData) -> None: block = w3.eth.get_block(empty_block["hash"]) assert block["hash"] == empty_block["hash"] diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index a03ff04b44..79f0d76eda 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -337,6 +337,7 @@ async def estimate_gas( self, transaction: Optional[TxParams] = None, block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, ) -> int: setup_transaction = self._estimate_gas(transaction) return await async_estimate_gas_for_function( @@ -347,6 +348,7 @@ async def estimate_gas( self.contract_abi, self.abi, block_identifier, + state_override, *self.args, **self.kwargs, ) diff --git a/web3/contract/contract.py b/web3/contract/contract.py index f617cfb608..f14ffbc829 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -335,6 +335,7 @@ def estimate_gas( self, transaction: Optional[TxParams] = None, block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, ) -> int: setup_transaction = self._estimate_gas(transaction) return estimate_gas_for_function( @@ -345,6 +346,7 @@ def estimate_gas( self.contract_abi, self.abi, block_identifier, + state_override, *self.args, **self.kwargs, ) diff --git a/web3/contract/utils.py b/web3/contract/utils.py index f0dd8b1471..445836d870 100644 --- a/web3/contract/utils.py +++ b/web3/contract/utils.py @@ -181,6 +181,7 @@ def estimate_gas_for_function( contract_abi: Optional[ABI] = None, fn_abi: Optional[ABIFunction] = None, block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, *args: Any, **kwargs: Any, ) -> int: @@ -200,7 +201,7 @@ def estimate_gas_for_function( fn_kwargs=kwargs, ) - return w3.eth.estimate_gas(estimate_transaction, block_identifier) + return w3.eth.estimate_gas(estimate_transaction, block_identifier, state_override) def build_transaction_for_function( @@ -389,6 +390,7 @@ async def async_estimate_gas_for_function( contract_abi: Optional[ABI] = None, fn_abi: Optional[ABIFunction] = None, block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, *args: Any, **kwargs: Any, ) -> int: @@ -408,7 +410,9 @@ async def async_estimate_gas_for_function( fn_kwargs=kwargs, ) - return await async_w3.eth.estimate_gas(estimate_transaction, block_identifier) + return await async_w3.eth.estimate_gas( + estimate_transaction, block_identifier, state_override + ) async def async_build_transaction_for_function( diff --git a/web3/eth/async_eth.py b/web3/eth/async_eth.py index 875716cd33..2bb3bfbbca 100644 --- a/web3/eth/async_eth.py +++ b/web3/eth/async_eth.py @@ -316,13 +316,19 @@ async def create_access_list( # eth_estimateGas _estimate_gas: Method[ - Callable[[TxParams, Optional[BlockIdentifier]], Awaitable[int]] + Callable[ + [TxParams, Optional[BlockIdentifier], Optional[CallOverride]], + Awaitable[int], + ] ] = Method(RPC.eth_estimateGas, mungers=[BaseEth.estimate_gas_munger]) async def estimate_gas( - self, transaction: TxParams, block_identifier: Optional[BlockIdentifier] = None + self, + transaction: TxParams, + block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, ) -> int: - return await self._estimate_gas(transaction, block_identifier) + return await self._estimate_gas(transaction, block_identifier, state_override) # eth_getTransactionByHash diff --git a/web3/eth/base_eth.py b/web3/eth/base_eth.py index 42d5d84eae..ac14805252 100644 --- a/web3/eth/base_eth.py +++ b/web3/eth/base_eth.py @@ -3,7 +3,6 @@ List, NoReturn, Optional, - Sequence, Tuple, Union, ) @@ -94,18 +93,38 @@ def set_gas_price_strategy( ) -> None: self._gas_price_strategy = gas_price_strategy - def estimate_gas_munger( - self, transaction: TxParams, block_identifier: Optional[BlockIdentifier] = None - ) -> Sequence[Union[TxParams, BlockIdentifier]]: + def _eth_call_and_estimate_gas_munger( + self, + transaction: TxParams, + block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, + ) -> Union[ + Tuple[TxParams, BlockIdentifier], Tuple[TxParams, BlockIdentifier, CallOverride] + ]: + # TODO: move to middleware if "from" not in transaction and is_checksum_address(self.default_account): transaction = assoc(transaction, "from", self.default_account) + # TODO: move to middleware if block_identifier is None: - params: Sequence[Union[TxParams, BlockIdentifier]] = [transaction] + block_identifier = self.default_block + + if state_override is None: + return (transaction, block_identifier) else: - params = [transaction, block_identifier] + return (transaction, block_identifier, state_override) - return params + def estimate_gas_munger( + self, + transaction: TxParams, + block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, + ) -> Union[ + Tuple[TxParams, BlockIdentifier], Tuple[TxParams, BlockIdentifier, CallOverride] + ]: + return self._eth_call_and_estimate_gas_munger( + transaction, block_identifier, state_override + ) def get_block_munger( self, block_identifier: BlockIdentifier, full_transactions: bool = False @@ -139,18 +158,9 @@ def call_munger( ) -> Union[ Tuple[TxParams, BlockIdentifier], Tuple[TxParams, BlockIdentifier, CallOverride] ]: - # TODO: move to middleware - if "from" not in transaction and is_checksum_address(self.default_account): - transaction = assoc(transaction, "from", self.default_account) - - # TODO: move to middleware - if block_identifier is None: - block_identifier = self.default_block - - if state_override is None: - return (transaction, block_identifier) - else: - return (transaction, block_identifier, state_override) + return self._eth_call_and_estimate_gas_munger( + transaction, block_identifier, state_override + ) def create_access_list_munger( self, transaction: TxParams, block_identifier: Optional[BlockIdentifier] = None diff --git a/web3/eth/eth.py b/web3/eth/eth.py index c8f737959c..24ae8ef7e8 100644 --- a/web3/eth/eth.py +++ b/web3/eth/eth.py @@ -302,14 +302,17 @@ def create_access_list( # eth_estimateGas - _estimate_gas: Method[Callable[[TxParams, Optional[BlockIdentifier]], int]] = ( - Method(RPC.eth_estimateGas, mungers=[BaseEth.estimate_gas_munger]) - ) + _estimate_gas: Method[ + Callable[[TxParams, Optional[BlockIdentifier], Optional[CallOverride]], int] + ] = Method(RPC.eth_estimateGas, mungers=[BaseEth.estimate_gas_munger]) def estimate_gas( - self, transaction: TxParams, block_identifier: Optional[BlockIdentifier] = None + self, + transaction: TxParams, + block_identifier: Optional[BlockIdentifier] = None, + state_override: Optional[CallOverride] = None, ) -> int: - return self._estimate_gas(transaction, block_identifier) + return self._estimate_gas(transaction, block_identifier, state_override) # eth_getTransactionByHash