Skip to content

Commit

Permalink
Async eth_call (#2083)
Browse files Browse the repository at this point in the history
* Async eth_call

* Update docs for supported async middleware and methods
  • Loading branch information
kclowes authored Jul 23, 2021
1 parent ec70be9 commit 8586a00
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 27 deletions.
25 changes: 18 additions & 7 deletions docs/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,27 @@ Pythonic

Gas Price Strategy
~~~~~~~~~~~~~~~~~~~~~~~~
.. warning::
Gas price strategy is only supported for legacy transactions. The London fork
introduced ``maxFeePerGas`` and ``maxPriorityFeePerGas`` transaction parameters
which should be used over ``gasPrice`` whenever possible.


.. py:method:: web3.middleware.gas_price_strategy_middleware
This adds a gasPrice to transactions if applicable and when a gas price strategy has
been set. See :ref:`Gas_Price` for information about how gas price is derived.
.. warning::

Gas price strategy is only supported for legacy transactions. The London fork
introduced ``maxFeePerGas`` and ``maxPriorityFeePerGas`` transaction parameters
which should be used over ``gasPrice`` whenever possible.

This adds a gasPrice to transactions if applicable and when a gas price strategy has
been set. See :ref:`Gas_Price` for information about how gas price is derived.

Buffered Gas Estimate
~~~~~~~~~~~~~~~~~~~~~~~~

.. py:method:: web3.middleware.buffered_gas_estimate_middleware
This adds a gas estimate to transactions if ``gas`` is not present in the transaction
parameters. Sets gas to:
``min(w3.eth.estimate_gas + gas_buffer, gas_limit)``
where the gas_buffer default is 100,000 Wei

HTTPRequestRetry
~~~~~~~~~~~~~~~~~~
Expand Down
16 changes: 16 additions & 0 deletions docs/providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -391,15 +391,31 @@ AsyncHTTPProvider
Supported Methods
^^^^^^^^^^^^^^^^^

Eth
***
- :meth:`web3.eth.block_number <web3.eth.Eth.block_number>`
- :meth:`web3.eth.coinbase <web3.eth.Eth.coinbase>`
- :meth:`web3.eth.gas_price <web3.eth.Eth.gas_price>`
- :meth:`web3.eth.call() <web3.eth.Eth.call>`
- :meth:`web3.eth.estimate_gas() <web3.eth.Eth.estimate_gas>`
- :meth:`web3.eth.generate_gas_price() <web3.eth.Eth.generate_gas_price>`
- :meth:`web3.eth.get_balance() <web3.eth.Eth.get_balance>`
- :meth:`web3.eth.get_block() <web3.eth.Eth.get_block>`
- :meth:`web3.eth.get_code() <web3.eth.Eth.get_code>`
- :meth:`web3.eth.get_raw_transaction() <web3.eth.Eth.get_raw_transaction>`
- :meth:`web3.eth.get_transaction() <web3.eth.Eth.get_transaction>`
- :meth:`web3.eth.get_transaction_count() <web3.eth.Eth.get_transaction_count>`
- :meth:`web3.eth.send_transaction() <web3.eth.Eth.send_transaction>`

Net
***
- :meth:`web3.net.listening() <web3.net.listening>`
- :meth:`web3.net.peer_count() <web3.net.peer_count>`
- :meth:`web3.net.version() <web3.net.version>`



Supported Middleware
^^^^^^^^^^^^^^^^^^^^
- :meth:`Gas Price Strategy <web3.middleware.gas_price_strategy_middleware>`
- :meth:`Buffered Gas Estimate Middleware <web3.middleware.buffered_gas_estimate_middleware>`
1 change: 1 addition & 0 deletions newsfragments/2083.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for async ``w3.eth.call``.
88 changes: 88 additions & 0 deletions web3/_utils/module_testing/eth_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,94 @@ async def test_eth_get_transaction_count(
assert is_integer(transaction_count)
assert transaction_count >= 0

@pytest.mark.asyncio
async def test_eth_call(
self, async_w3: "Web3", math_contract: "Contract"
) -> None:
coinbase = await async_w3.eth.coinbase # type: ignore
txn_params = math_contract._prepare_transaction(
fn_name='add',
fn_args=(7, 11),
transaction={'from': coinbase, 'to': math_contract.address},
)
call_result = await async_w3.eth.call(txn_params) # type: ignore
assert is_string(call_result)
result = async_w3.codec.decode_single('uint256', call_result)
assert result == 18

@pytest.mark.asyncio
async def test_eth_call_with_override(
self, async_w3: "Web3", revert_contract: "Contract"
) -> None:
coinbase = await async_w3.eth.coinbase # type: ignore
txn_params = revert_contract._prepare_transaction(
fn_name='normalFunction',
transaction={'from': coinbase, 'to': revert_contract.address},
)
call_result = await async_w3.eth.call(txn_params) # type: ignore
result = async_w3.codec.decode_single('bool', call_result)
assert result is True

# override runtime bytecode: `normalFunction` returns `false`
override_code = '0x6080604052348015600f57600080fd5b5060043610603c5760003560e01c8063185c38a4146041578063c06a97cb146049578063d67e4b84146051575b600080fd5b60476071565b005b604f60df565b005b605760e4565b604051808215151515815260200191505060405180910390f35b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f46756e6374696f6e20686173206265656e2072657665727465642e000000000081525060200191505060405180910390fd5b600080fd5b60008090509056fea2646970667358221220bb71e9e9a2e271cd0fbe833524a3ea67df95f25ea13aef5b0a761fa52b538f1064736f6c63430006010033' # noqa: E501
call_result = await async_w3.eth.call( # type: ignore
txn_params,
'latest',
{revert_contract.address: {'code': override_code}}
)
result = async_w3.codec.decode_single('bool', call_result)
assert result is False

@pytest.mark.asyncio
async def test_eth_call_with_0_result(
self, async_w3: "Web3", math_contract: "Contract"
) -> None:
coinbase = await async_w3.eth.coinbase # type: ignore
txn_params = math_contract._prepare_transaction(
fn_name='add',
fn_args=(0, 0),
transaction={'from': coinbase, 'to': math_contract.address},
)
call_result = await async_w3.eth.call(txn_params) # type: ignore
assert is_string(call_result)
result = async_w3.codec.decode_single('uint256', call_result)
assert result == 0

@pytest.mark.asyncio
async def test_eth_call_revert_with_msg(
self,
async_w3: "Web3",
revert_contract: "Contract",
unlocked_account: ChecksumAddress,
) -> None:
with pytest.raises(ContractLogicError,
match='execution reverted: Function has been reverted'):
txn_params = revert_contract._prepare_transaction(
fn_name="revertWithMessage",
transaction={
"from": unlocked_account,
"to": revert_contract.address,
},
)
await async_w3.eth.call(txn_params) # type: ignore

@pytest.mark.asyncio
async def test_eth_call_revert_without_msg(
self,
async_w3: "Web3",
revert_contract: "Contract",
unlocked_account: ChecksumAddress,
) -> None:
with pytest.raises(ContractLogicError, match="execution reverted"):
txn_params = revert_contract._prepare_transaction(
fn_name="revertWithoutMessage",
transaction={
"from": unlocked_account,
"to": revert_contract.address,
},
)
await async_w3.eth.call(txn_params) # type: ignore


class EthModuleTest:
def test_eth_protocol_version(self, web3: "Web3") -> None:
Expand Down
53 changes: 33 additions & 20 deletions web3/eth.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ def block_id_munger(
block_identifier = self.default_block
return (account, block_identifier)

def call_munger(
self,
transaction: TxParams,
block_identifier: Optional[BlockIdentifier] = None,
state_override: Optional[CallOverrideParams] = None,
) -> Union[Tuple[TxParams, BlockIdentifier], Tuple[TxParams, BlockIdentifier, CallOverrideParams]]: # noqa-E501
# 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)


class AsyncEth(BaseEth):
is_async = True
Expand Down Expand Up @@ -299,6 +318,19 @@ async def get_transaction_count(
) -> Nonce:
return await self._get_transaction_count(account, block_identifier)

_call: Method[Callable[..., Awaitable[Union[bytes, bytearray]]]] = Method(
RPC.eth_call,
mungers=[BaseEth.call_munger]
)

async def call(
self,
transaction: TxParams,
block_identifier: Optional[BlockIdentifier] = None,
state_override: Optional[CallOverrideParams] = None,
) -> Union[bytes, bytearray]:
return await self._call(transaction, block_identifier, state_override)


class Eth(BaseEth, Module):
account = Account()
Expand Down Expand Up @@ -654,28 +686,9 @@ def sign_munger(
mungers=[default_root_munger],
)

def call_munger(
self,
transaction: TxParams,
block_identifier: Optional[BlockIdentifier] = None,
state_override: Optional[CallOverrideParams] = None,
) -> Union[Tuple[TxParams, BlockIdentifier], Tuple[TxParams, BlockIdentifier, CallOverrideParams]]: # noqa-E501
# 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)

call: Method[Callable[..., Union[bytes, bytearray]]] = Method(
RPC.eth_call,
mungers=[call_munger]
mungers=[BaseEth.call_munger]
)

def estimate_gas(
Expand Down

0 comments on commit 8586a00

Please sign in to comment.