diff --git a/docs/contracts.rst b/docs/contracts.rst index cd5cedf513..b2e51ad935 100644 --- a/docs/contracts.rst +++ b/docs/contracts.rst @@ -247,6 +247,11 @@ Each Contract Factory exposes the following methods. .. py:classmethod:: Contract.constructor(*args, **kwargs).estimateGas(transaction=None, block_identifier=None) :noindex: + .. warning:: Deprecated: This method is deprecated in favor of :py:meth:`Contract.constructor(*args, **kwargs).estimate_gas` + +.. py:classmethod:: Contract.constructor(*args, **kwargs).estimate_gas(transaction=None, block_identifier=None) + :noindex: + Estimate gas for constructing and deploying the contract. This method behaves the same as the @@ -264,12 +269,17 @@ Each Contract Factory exposes the following methods. .. code-block:: python - >>> token_contract.constructor(web3.eth.coinbase, 12345).estimateGas() + >>> token_contract.constructor(web3.eth.coinbase, 12345).estimate_gas() 12563 .. py:classmethod:: Contract.constructor(*args, **kwargs).buildTransaction(transaction=None) :noindex: + .. warning:: Deprecated: This method is deprecated in favor of :py:meth:`Contract.constructor(*args, **kwargs).build_transaction` + +.. py:classmethod:: Contract.constructor(*args, **kwargs).build_transaction(transaction=None) + :noindex: + Construct the contract deploy transaction bytecode data. If the contract takes constructor parameters they should be provided as @@ -286,7 +296,7 @@ Each Contract Factory exposes the following methods. 'gasPrice': w3.eth.gas_price, 'chainId': None } - >>> contract_data = token_contract.constructor(web3.eth.coinbase, 12345).buildTransaction(transaction) + >>> contract_data = token_contract.constructor(web3.eth.coinbase, 12345).build_transaction(transaction) >>> web3.eth.send_transaction(contract_data) .. _contract_createFilter: @@ -835,6 +845,10 @@ Methods .. py:method:: ContractFunction.estimateGas(transaction, block_identifier=None) + .. warning:: Deprecated: This method is deprecated in favor of :class:`~estimate_gas` + +.. py:method:: ContractFunction.estimate_gas(transaction, block_identifier=None) + Call a contract function, executing the transaction locally using the ``eth_call`` API. This will not create a new public transaction. @@ -842,7 +856,7 @@ Methods .. code-block:: python - myContract.functions.myMethod(*args, **kwargs).estimateGas(transaction) + myContract.functions.myMethod(*args, **kwargs).estimate_gas(transaction) This method behaves the same as the :py:meth:`ContractFunction.transact` method, with transaction details being passed into the end portion of the @@ -853,7 +867,7 @@ Methods .. code-block:: python - >>> my_contract.functions.multiply7(3).estimateGas() + >>> my_contract.functions.multiply7(3).estimate_gas() 42650 .. note:: @@ -863,13 +877,17 @@ Methods .. py:method:: ContractFunction.buildTransaction(transaction) + .. warning:: Deprecated: This method is deprecated in favor of :class:`~build_transaction` + +.. py:method:: ContractFunction.build_transaction(transaction) + Builds a transaction dictionary based on the contract function call specified. Refer to the following invocation: .. code-block:: python - myContract.functions.myMethod(*args, **kwargs).buildTransaction(transaction) + myContract.functions.myMethod(*args, **kwargs).build_transaction(transaction) This method behaves the same as the :py:meth:`Contract.transact` method, with transaction details being passed into the end portion of the @@ -881,7 +899,7 @@ Methods .. code-block:: python - >>> math_contract.functions.increment(5).buildTransaction({'nonce': 10}) + >>> math_contract.functions.increment(5).build_transaction({'nonce': 10}) You may use :meth:`~web3.eth.Eth.getTransactionCount` to get the current nonce for an account. Therefore a shortcut for producing a transaction dictionary with @@ -889,7 +907,7 @@ Methods .. code-block:: python - >>> math_contract.functions.increment(5).buildTransaction({'nonce': web3.eth.get_transaction_count('0xF5...')}) + >>> math_contract.functions.increment(5).build_transaction({'nonce': web3.eth.get_transaction_count('0xF5...')}) Returns a transaction dictionary. This transaction dictionary can then be sent using :meth:`~web3.eth.Eth.send_transaction`. @@ -899,7 +917,7 @@ Methods .. code-block:: python - >>> math_contract.functions.increment(5).buildTransaction({'maxFeePerGas': 2000000000, 'maxPriorityFeePerGas': 1000000000}) + >>> math_contract.functions.increment(5).build_transaction({'maxFeePerGas': 2000000000, 'maxPriorityFeePerGas': 1000000000}) { 'to': '0x6Bc272FCFcf89C14cebFC57B8f1543F5137F97dE', 'data': '0x7cf5dab00000000000000000000000000000000000000000000000000000000000000005', diff --git a/docs/providers.rst b/docs/providers.rst index cb1395ecd7..e2cdc2e5bf 100644 --- a/docs/providers.rst +++ b/docs/providers.rst @@ -468,6 +468,12 @@ Geth - :meth:`web3.geth.txpool.content() ` - :meth:`web3.geth.txpool.status() ` +Contract +^^^^^^^^ +Contract is fully implemented for the Async provider. The only documented exception to this at +the moment is where :class:`ENS` is needed for address lookup. All addresses that are passed to Async +contract should not be :class:`ENS` addresses. + Supported Middleware ^^^^^^^^^^^^^^^^^^^^ - :meth:`Gas Price Strategy ` diff --git a/tests/core/conftest.py b/tests/core/conftest.py index 6947f8d334..ee8a81b91a 100644 --- a/tests/core/conftest.py +++ b/tests/core/conftest.py @@ -1,8 +1,17 @@ import pytest +import pytest_asyncio + +from web3 import Web3 +from web3.eth import ( + AsyncEth, +) from web3.module import ( Module, ) +from web3.providers.eth_tester.main import ( + AsyncEthereumTesterProvider, +) # --- inherit from `web3.module.Module` class --- # @@ -97,3 +106,12 @@ def __init__(self, a, b): self.a = a self.b = b return ModuleManyArgs + + +@pytest_asyncio.fixture() +async def async_w3(): + provider = AsyncEthereumTesterProvider() + w3 = Web3(provider, modules={'eth': [AsyncEth]}, + middlewares=provider.middlewares) + w3.eth.default_account = await w3.eth.coinbase + return w3 diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index ab9d5abd12..8fd41bc910 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -10,7 +10,6 @@ ) import pytest_asyncio -from web3 import Web3 from web3._utils.module_testing.emitter_contract import ( CONTRACT_EMITTER_ABI, CONTRACT_EMITTER_CODE, @@ -54,12 +53,6 @@ from web3.contract import ( AsyncContract, ) -from web3.eth import ( - AsyncEth, -) -from web3.providers.eth_tester.main import ( - AsyncEthereumTesterProvider, -) CONTRACT_NESTED_TUPLE_SOURCE = """ pragma solidity >=0.4.19 <0.6.0; @@ -1064,15 +1057,6 @@ async def async_deploy(async_web3, Contract, apply_func=identity, args=None): return contract -@pytest_asyncio.fixture() -async def async_w3(): - provider = AsyncEthereumTesterProvider() - w3 = Web3(provider, modules={'eth': [AsyncEth]}, - middlewares=provider.middlewares) - w3.eth.default_account = await w3.eth.coinbase - return w3 - - @pytest_asyncio.fixture() def AsyncMathContract(async_w3, MATH_ABI, MATH_CODE, MATH_RUNTIME): contract = AsyncContract.factory(async_w3, diff --git a/tests/core/utilities/test_async_transaction.py b/tests/core/utilities/test_async_transaction.py new file mode 100644 index 0000000000..6287fd8665 --- /dev/null +++ b/tests/core/utilities/test_async_transaction.py @@ -0,0 +1,22 @@ +import pytest + +from web3._utils.async_transactions import ( + fill_transaction_defaults, +) + + +@pytest.mark.asyncio() +async def test_fill_transaction_defaults_for_all_params(async_w3): + default_transaction = await fill_transaction_defaults(async_w3, {}) + + block = await async_w3.eth.get_block('latest') + assert default_transaction == { + 'chainId': await async_w3.eth.chain_id, + 'data': b'', + 'gas': await async_w3.eth.estimate_gas({}), + 'maxFeePerGas': ( + await async_w3.eth.max_priority_fee + (2 * block['baseFeePerGas']) + ), + 'maxPriorityFeePerGas': await async_w3.eth.max_priority_fee, + 'value': 0, + } diff --git a/web3/_utils/async_transactions.py b/web3/_utils/async_transactions.py index 7e6153e047..0bd50479c7 100644 --- a/web3/_utils/async_transactions.py +++ b/web3/_utils/async_transactions.py @@ -1,12 +1,25 @@ from typing import ( TYPE_CHECKING, + Awaitable, Optional, cast, ) +from eth_utils.toolz import ( + curry, + merge, +) + +from web3._utils.utility_methods import ( + any_in_dict, +) +from web3.constants import ( + DYNAMIC_FEE_TXN_PARAMS, +) from web3.types import ( BlockIdentifier, TxParams, + Wei, ) if TYPE_CHECKING: @@ -14,6 +27,37 @@ from web3.eth import AsyncEth # noqa: F401 +async def _estimate_gas(w3: 'Web3', tx: TxParams) -> Awaitable[int]: + return await w3.eth.estimate_gas(tx) # type: ignore + + +async def _gas_price(w3: 'Web3', tx: TxParams) -> Awaitable[Optional[Wei]]: + return await w3.eth.generate_gas_price(tx) or w3.eth.gas_price # type: ignore + + +async def _max_fee_per_gas(w3: 'Web3', tx: TxParams) -> Awaitable[Wei]: + block = await w3.eth.get_block('latest') # type: ignore + return await w3.eth.max_priority_fee + (2 * block['baseFeePerGas']) # type: ignore + + +async def _max_priority_fee_gas(w3: 'Web3', tx: TxParams) -> Awaitable[Wei]: + return await w3.eth.max_priority_fee # type: ignore + + +async def _chain_id(w3: 'Web3', tx: TxParams) -> Awaitable[int]: + return await w3.eth.chain_id # type: ignore + +TRANSACTION_DEFAULTS = { + 'value': 0, + 'data': b'', + 'gas': _estimate_gas, + 'gasPrice': _gas_price, + 'maxFeePerGas': _max_fee_per_gas, + 'maxPriorityFeePerGas': _max_priority_fee_gas, + 'chainId': _chain_id, +} + + async def get_block_gas_limit( web3_eth: "AsyncEth", block_identifier: Optional[BlockIdentifier] = None ) -> int: @@ -40,3 +84,38 @@ async def get_buffered_gas_estimate( ) return min(gas_limit, gas_estimate + gas_buffer) + + +@curry +async def fill_transaction_defaults(w3: "Web3", transaction: TxParams) -> TxParams: + """ + if w3 is None, fill as much as possible while offline + """ + strategy_based_gas_price = await w3.eth.generate_gas_price(transaction) # type: ignore + is_dynamic_fee_transaction = ( + not strategy_based_gas_price + and ( + 'gasPrice' not in transaction # default to dynamic fee transaction + or any_in_dict(DYNAMIC_FEE_TXN_PARAMS, transaction) + ) + ) + + defaults = {} + for key, default_getter in TRANSACTION_DEFAULTS.items(): + if key not in transaction: + if ( + is_dynamic_fee_transaction and key == 'gasPrice' + or not is_dynamic_fee_transaction and key in DYNAMIC_FEE_TXN_PARAMS + ): + # do not set default max fees if legacy txn or gas price if dynamic fee txn + continue + + if callable(default_getter): + if w3 is None: + raise ValueError(f"You must specify a '{key}' value in the transaction") + default_val = await default_getter(w3, transaction) + else: + default_val = default_getter + + defaults[key] = default_val + return merge(defaults, transaction) diff --git a/web3/contract.py b/web3/contract.py index 26ed67e162..aa4cd268b9 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -49,6 +49,10 @@ HexBytes, ) +from web3._utils import ( + async_transactions, + transactions, +) from web3._utils.abi import ( abi_to_signature, check_if_arguments_can_be_encoded, @@ -106,9 +110,6 @@ normalize_address_no_ens, normalize_bytecode, ) -from web3._utils.transactions import ( - fill_transaction_defaults, -) from web3.datastructures import ( AttributeDict, MutableAttributeDict, @@ -802,10 +803,9 @@ def _encode_data_in_transaction(self, *args: Any, **kwargs: Any) -> HexStr: return data @combomethod - def estimateGas( - self, transaction: Optional[TxParams] = None, - block_identifier: Optional[BlockIdentifier] = None - ) -> int: + def _estimate_gas( + self, transaction: Optional[TxParams] = None + ) -> TxParams: if transaction is None: estimate_gas_transaction: TxParams = {} else: @@ -819,9 +819,7 @@ def estimateGas( estimate_gas_transaction['data'] = self.data_in_transaction - return self.w3.eth.estimate_gas( - estimate_gas_transaction, block_identifier=block_identifier - ) + return estimate_gas_transaction def _get_transaction(self, transaction: Optional[TxParams] = None) -> TxParams: if transaction is None: @@ -868,7 +866,7 @@ def build_transaction(self, transaction: Optional[TxParams] = None) -> TxParams: Build the transaction dictionary without sending """ built_transaction = self._build_transaction(transaction) - return fill_transaction_defaults(self.w3, built_transaction) + return transactions.fill_transaction_defaults(self.w3, built_transaction) @combomethod @deprecated_for("build_transaction") @@ -878,6 +876,25 @@ def buildTransaction(self, transaction: Optional[TxParams] = None) -> TxParams: """ return self.build_transaction(transaction) + @combomethod + @deprecated_for("estimate_gas") + def estimateGas( + self, transaction: Optional[TxParams] = None, + block_identifier: Optional[BlockIdentifier] = None + ) -> int: + return self.estimate_gas(transaction, block_identifier) + + @combomethod + def estimate_gas( + self, transaction: Optional[TxParams] = None, + block_identifier: Optional[BlockIdentifier] = None + ) -> int: + transaction = self._estimate_gas(transaction) + + return self.w3.eth.estimate_gas( + transaction, block_identifier=block_identifier + ) + class AsyncContractConstructor(BaseContractConstructor): @@ -892,7 +909,18 @@ async def build_transaction(self, transaction: Optional[TxParams] = None) -> TxP Build the transaction dictionary without sending """ built_transaction = self._build_transaction(transaction) - return fill_transaction_defaults(self.w3, built_transaction) + return async_transactions.fill_transaction_defaults(self.w3, built_transaction) + + @combomethod + async def estimate_gas( + self, transaction: Optional[TxParams] = None, + block_identifier: Optional[BlockIdentifier] = None + ) -> int: + transaction = self._estimate_gas(transaction) + + return await self.w3.eth.estimate_gas( # type: ignore + transaction, block_identifier=block_identifier + ) class ConciseMethod: @@ -1172,10 +1200,7 @@ def _estimate_gas( ) return estimate_gas_transaction - def buildTransaction(self, transaction: Optional[TxParams] = None) -> TxParams: - """ - Build the transaction dictionary without sending - """ + def _build_transaction(self, transaction: Optional[TxParams] = None) -> TxParams: if transaction is None: built_transaction: TxParams = {} else: @@ -1200,16 +1225,7 @@ def buildTransaction(self, transaction: Optional[TxParams] = None) -> TxParams: "Please ensure that this contract instance has an address." ) - return build_transaction_for_function( - self.address, - self.w3, - self.function_identifier, - built_transaction, - self.contract_abi, - self.abi, - *self.args, - **self.kwargs - ) + return built_transaction @combomethod def _encode_transaction_data(cls) -> HexStr: @@ -1330,6 +1346,24 @@ def estimateGas( ) -> int: return self.estimate_gas(transaction, block_identifier) + def build_transaction(self, transaction: Optional[TxParams] = None) -> TxParams: + + built_transaction = self._build_transaction(transaction) + return build_transaction_for_function( + self.address, + self.w3, + self.function_identifier, + built_transaction, + self.contract_abi, + self.abi, + *self.args, + **self.kwargs + ) + + @deprecated_for("build_transaction") + def buildTransaction(self, transaction: Optional[TxParams] = None) -> TxParams: + return self.build_transaction(transaction) + class AsyncContractFunction(BaseContractFunction): @@ -1430,6 +1464,20 @@ async def estimate_gas( **self.kwargs ) + async def build_transaction(self, transaction: Optional[TxParams] = None) -> TxParams: + + built_transaction = self._build_transaction(transaction) + return await async_build_transaction_for_function( + self.address, + self.w3, + self.function_identifier, + built_transaction, + self.contract_abi, + self.abi, + *self.args, + **self.kwargs + ) + class BaseContractEvent: """Base class for contract events @@ -2247,7 +2295,38 @@ def build_transaction_for_function( fn_kwargs=kwargs, ) - prepared_transaction = fill_transaction_defaults(w3, prepared_transaction) + prepared_transaction = transactions.fill_transaction_defaults(w3, prepared_transaction) + + return prepared_transaction + + +async def async_build_transaction_for_function( + address: ChecksumAddress, + w3: 'Web3', + function_name: Optional[FunctionIdentifier] = None, + transaction: Optional[TxParams] = None, + contract_abi: Optional[ABI] = None, + fn_abi: Optional[ABIFunction] = None, + *args: Any, + **kwargs: Any) -> TxParams: + """Builds a dictionary with the fields required to make the given transaction + + Don't call this directly, instead use :meth:`Contract.buildTransaction` + on your contract instance. + """ + prepared_transaction = prepare_transaction( + address, + w3, + fn_identifier=function_name, + contract_abi=contract_abi, + fn_abi=fn_abi, + transaction=transaction, + fn_args=args, + fn_kwargs=kwargs, + ) + + prepared_transaction = await async_transactions.fill_transaction_defaults( + w3, prepared_transaction) return prepared_transaction