Skip to content

[V6] Estimate Gas with State Override #3220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 5, 2024
Merged
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
6 changes: 5 additions & 1 deletion docs/web3.eth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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})
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3164.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement ``state_override`` parameter for ``eth_estimateGas`` method.
4 changes: 4 additions & 0 deletions tests/integration/test_ethereum_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 21 additions & 1 deletion web3/_utils/method_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,14 +455,33 @@ 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(
[
transaction_param_formatter,
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,
Expand Down Expand Up @@ -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),
Expand Down
63 changes: 63 additions & 0 deletions web3/_utils/module_testing/eth_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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"]
Expand Down
2 changes: 2 additions & 0 deletions web3/contract/async_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -347,6 +348,7 @@ async def estimate_gas(
self.contract_abi,
self.abi,
block_identifier,
state_override,
*self.args,
**self.kwargs,
)
Expand Down
2 changes: 2 additions & 0 deletions web3/contract/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -345,6 +346,7 @@ def estimate_gas(
self.contract_abi,
self.abi,
block_identifier,
state_override,
*self.args,
**self.kwargs,
)
Expand Down
8 changes: 6 additions & 2 deletions web3/contract/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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(
Expand Down
12 changes: 9 additions & 3 deletions web3/eth/async_eth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 29 additions & 19 deletions web3/eth/base_eth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
List,
NoReturn,
Optional,
Sequence,
Tuple,
Union,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions web3/eth/eth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down