Skip to content

Commit 0896611

Browse files
dbfreemfselmo
authored andcommitted
More async contract:
- setting default contract factory in AsyncEth to AsyncContract - estimateGas async in Contract and docs - build_transaction in ContractFunction - fill_transaction_defaults async for later use in build_transaction_for_function - async_parse_block_identifier_int - ContractConstructor - Create Base/Async structure for ContractFunctions, ContractEvents, Contract, ContractConstructor, ContractFunction, ContractEvent, ContractCaller classes - fixing combomethod for async support - fix test import in the wrong place - fixed the type in the cast of get_receive_function and get_fallback_function - rearranged BaseContractFunction.factory - removed async_fill_default from ethtester middleware - changed fill_transaction_defaults imports - fixed bug with async_call_contract_function - linting, linting, and more linting - docs
1 parent c1cb4e3 commit 0896611

File tree

12 files changed

+751
-222
lines changed

12 files changed

+751
-222
lines changed

docs/contracts.rst

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ Each Contract Factory exposes the following methods.
247247
.. py:classmethod:: Contract.constructor(*args, **kwargs).estimateGas(transaction=None, block_identifier=None)
248248
:noindex:
249249

250+
.. warning:: Deprecated: This method is deprecated in favor of :py:meth:`Contract.constructor(*args, **kwargs).estimate_gas`
251+
252+
.. py:classmethod:: Contract.constructor(*args, **kwargs).estimate_gas(transaction=None, block_identifier=None)
253+
:noindex:
254+
250255
Estimate gas for constructing and deploying the contract.
251256

252257
This method behaves the same as the
@@ -264,12 +269,17 @@ Each Contract Factory exposes the following methods.
264269

265270
.. code-block:: python
266271
267-
>>> token_contract.constructor(web3.eth.coinbase, 12345).estimateGas()
272+
>>> token_contract.constructor(web3.eth.coinbase, 12345).estimate_gas()
268273
12563
269274
270275
.. py:classmethod:: Contract.constructor(*args, **kwargs).buildTransaction(transaction=None)
271276
:noindex:
272277

278+
.. warning:: Deprecated: This method is deprecated in favor of :py:meth:`Contract.constructor(*args, **kwargs).build_transaction`
279+
280+
.. py:classmethod:: Contract.constructor(*args, **kwargs).build_transaction(transaction=None)
281+
:noindex:
282+
273283
Construct the contract deploy transaction bytecode data.
274284

275285
If the contract takes constructor parameters they should be provided as
@@ -286,7 +296,7 @@ Each Contract Factory exposes the following methods.
286296
'gasPrice': w3.eth.gas_price,
287297
'chainId': None
288298
}
289-
>>> contract_data = token_contract.constructor(web3.eth.coinbase, 12345).buildTransaction(transaction)
299+
>>> contract_data = token_contract.constructor(web3.eth.coinbase, 12345).build_transaction(transaction)
290300
>>> web3.eth.send_transaction(contract_data)
291301
292302
.. _contract_createFilter:
@@ -835,14 +845,18 @@ Methods
835845

836846
.. py:method:: ContractFunction.estimateGas(transaction, block_identifier=None)
837847
848+
.. warning:: Deprecated: This method is deprecated in favor of :class:`~estimate_gas`
849+
850+
.. py:method:: ContractFunction.estimate_gas(transaction, block_identifier=None)
851+
838852
Call a contract function, executing the transaction locally using the
839853
``eth_call`` API. This will not create a new public transaction.
840854

841855
Refer to the following invocation:
842856

843857
.. code-block:: python
844858
845-
myContract.functions.myMethod(*args, **kwargs).estimateGas(transaction)
859+
myContract.functions.myMethod(*args, **kwargs).estimate_gas(transaction)
846860
847861
This method behaves the same as the :py:meth:`ContractFunction.transact` method,
848862
with transaction details being passed into the end portion of the
@@ -853,7 +867,7 @@ Methods
853867

854868
.. code-block:: python
855869
856-
>>> my_contract.functions.multiply7(3).estimateGas()
870+
>>> my_contract.functions.multiply7(3).estimate_gas()
857871
42650
858872
859873
.. note::
@@ -863,13 +877,17 @@ Methods
863877

864878
.. py:method:: ContractFunction.buildTransaction(transaction)
865879
880+
.. warning:: Deprecated: This method is deprecated in favor of :class:`~build_transaction`
881+
882+
.. py:method:: ContractFunction.build_transaction(transaction)
883+
866884
Builds a transaction dictionary based on the contract function call specified.
867885

868886
Refer to the following invocation:
869887

870888
.. code-block:: python
871889
872-
myContract.functions.myMethod(*args, **kwargs).buildTransaction(transaction)
890+
myContract.functions.myMethod(*args, **kwargs).build_transaction(transaction)
873891
874892
This method behaves the same as the :py:meth:`Contract.transact` method,
875893
with transaction details being passed into the end portion of the
@@ -881,15 +899,15 @@ Methods
881899

882900
.. code-block:: python
883901
884-
>>> math_contract.functions.increment(5).buildTransaction({'nonce': 10})
902+
>>> math_contract.functions.increment(5).build_transaction({'nonce': 10})
885903
886904
You may use :meth:`~web3.eth.Eth.getTransactionCount` to get the current nonce
887905
for an account. Therefore a shortcut for producing a transaction dictionary with
888906
nonce included looks like:
889907

890908
.. code-block:: python
891909
892-
>>> math_contract.functions.increment(5).buildTransaction({'nonce': web3.eth.get_transaction_count('0xF5...')})
910+
>>> math_contract.functions.increment(5).build_transaction({'nonce': web3.eth.get_transaction_count('0xF5...')})
893911
894912
Returns a transaction dictionary. This transaction dictionary can then be sent using
895913
:meth:`~web3.eth.Eth.send_transaction`.
@@ -899,7 +917,7 @@ Methods
899917

900918
.. code-block:: python
901919
902-
>>> math_contract.functions.increment(5).buildTransaction({'maxFeePerGas': 2000000000, 'maxPriorityFeePerGas': 1000000000})
920+
>>> math_contract.functions.increment(5).build_transaction({'maxFeePerGas': 2000000000, 'maxPriorityFeePerGas': 1000000000})
903921
{
904922
'to': '0x6Bc272FCFcf89C14cebFC57B8f1543F5137F97dE',
905923
'data': '0x7cf5dab00000000000000000000000000000000000000000000000000000000000000005',

docs/providers.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ Geth
468468
- :meth:`web3.geth.txpool.content() <web3.geth.txpool.TxPool.content()>`
469469
- :meth:`web3.geth.txpool.status() <web3.geth.txpool.TxPool.status()>`
470470

471+
Contract
472+
^^^^^^^^
473+
Contract is fully implemented for the Async provider. The only documented exception to this at
474+
the moment is where :class:`ENS` is needed for address lookup. All addresses that are passed to Async
475+
contract should not be :class:`ENS` addresses.
476+
471477
Supported Middleware
472478
^^^^^^^^^^^^^^^^^^^^
473479
- :meth:`Gas Price Strategy <web3.middleware.gas_price_strategy_middleware>`

ethpm/package.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ def get_contract_instance(self, name: ContractName, address: Address) -> Contrac
309309
contract_instance = self.w3.eth.contract(
310310
address=address, **contract_kwargs
311311
)
312-
return contract_instance
312+
# TODO: type ignore may be able to be removed after more of AsynContract is finished
313+
return contract_instance # type: ignore
313314

314315
#
315316
# Build Dependencies

tests/core/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import pytest
22

3+
import pytest_asyncio
4+
5+
from web3 import Web3
6+
from web3.eth import (
7+
AsyncEth,
8+
)
39
from web3.module import (
410
Module,
511
)
12+
from web3.providers.eth_tester.main import (
13+
AsyncEthereumTesterProvider,
14+
)
615

716
# --- inherit from `web3.module.Module` class --- #
817

@@ -97,3 +106,12 @@ def __init__(self, a, b):
97106
self.a = a
98107
self.b = b
99108
return ModuleManyArgs
109+
110+
111+
@pytest_asyncio.fixture()
112+
async def async_w3():
113+
provider = AsyncEthereumTesterProvider()
114+
w3 = Web3(provider, modules={'eth': [AsyncEth]},
115+
middlewares=provider.middlewares)
116+
w3.eth.default_account = await w3.eth.coinbase
117+
return w3

tests/core/contracts/conftest.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
)
1111
import pytest_asyncio
1212

13-
from web3 import Web3
1413
from web3._utils.module_testing.emitter_contract import (
1514
CONTRACT_EMITTER_ABI,
1615
CONTRACT_EMITTER_CODE,
@@ -54,12 +53,6 @@
5453
from web3.contract import (
5554
AsyncContract,
5655
)
57-
from web3.eth import (
58-
AsyncEth,
59-
)
60-
from web3.providers.eth_tester.main import (
61-
AsyncEthereumTesterProvider,
62-
)
6356

6457
CONTRACT_NESTED_TUPLE_SOURCE = """
6558
pragma solidity >=0.4.19 <0.6.0;
@@ -1052,28 +1045,18 @@ def buildTransaction(request):
10521045
return functools.partial(invoke_contract, api_call_desig='buildTransaction')
10531046

10541047

1055-
@pytest_asyncio.fixture()
1056-
async def async_deploy(web3, Contract, apply_func=identity, args=None):
1048+
async def async_deploy(async_web3, Contract, apply_func=identity, args=None):
10571049
args = args or []
10581050
deploy_txn = await Contract.constructor(*args).transact()
1059-
deploy_receipt = await web3.eth.wait_for_transaction_receipt(deploy_txn)
1051+
deploy_receipt = await async_web3.eth.wait_for_transaction_receipt(deploy_txn)
10601052
assert deploy_receipt is not None
10611053
address = apply_func(deploy_receipt['contractAddress'])
10621054
contract = Contract(address=address)
10631055
assert contract.address == address
1064-
assert len(await web3.eth.get_code(contract.address)) > 0
1056+
assert len(await async_web3.eth.get_code(contract.address)) > 0
10651057
return contract
10661058

10671059

1068-
@pytest_asyncio.fixture()
1069-
async def async_w3():
1070-
provider = AsyncEthereumTesterProvider()
1071-
w3 = Web3(provider, modules={'eth': [AsyncEth]},
1072-
middlewares=provider.middlewares)
1073-
w3.eth.default_account = await w3.eth.coinbase
1074-
return w3
1075-
1076-
10771060
@pytest_asyncio.fixture()
10781061
def AsyncMathContract(async_w3, MATH_ABI, MATH_CODE, MATH_RUNTIME):
10791062
contract = AsyncContract.factory(async_w3,

tests/core/contracts/test_contract_call_interface.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,3 +879,31 @@ def test_call_revert_contract(revert_contract):
879879
# which does not contain the revert reason. Avoid that by giving a gas
880880
# value.
881881
revert_contract.functions.revertWithMessage().call({'gas': 100000})
882+
883+
884+
@pytest.mark.asyncio
885+
async def test_async_call_with_no_arguments(async_math_contract, call):
886+
result = await async_math_contract.functions.return13().call()
887+
assert result == 13
888+
889+
890+
@pytest.mark.asyncio
891+
async def test_async_call_with_one_argument(async_math_contract, call):
892+
result = await async_math_contract.functions.multiply7(3).call()
893+
assert result == 21
894+
895+
896+
@pytest.mark.asyncio
897+
async def test_async_returns_data_from_specified_block(async_w3, async_math_contract):
898+
start_num = await async_w3.eth.get_block('latest')
899+
await async_w3.provider.make_request(method='evm_mine', params=[5])
900+
await async_math_contract.functions.increment().transact()
901+
await async_math_contract.functions.increment().transact()
902+
903+
output1 = await async_math_contract.functions.counter().call(
904+
block_identifier=start_num.number + 6)
905+
output2 = await async_math_contract.functions.counter().call(
906+
block_identifier=start_num.number + 7)
907+
908+
assert output1 == 1
909+
assert output2 == 2
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import pytest
2+
3+
from web3._utils.async_transactions import (
4+
fill_transaction_defaults,
5+
)
6+
7+
8+
@pytest.mark.asyncio()
9+
async def test_fill_transaction_defaults_for_all_params(async_w3):
10+
default_transaction = await fill_transaction_defaults(async_w3, {})
11+
12+
block = await async_w3.eth.get_block('latest')
13+
assert default_transaction == {
14+
'chainId': await async_w3.eth.chain_id,
15+
'data': b'',
16+
'gas': await async_w3.eth.estimate_gas({}),
17+
'maxFeePerGas': (
18+
await async_w3.eth.max_priority_fee + (2 * block['baseFeePerGas'])
19+
),
20+
'maxPriorityFeePerGas': await async_w3.eth.max_priority_fee,
21+
'value': 0,
22+
}

web3/_utils/async_transactions.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,63 @@
11
from typing import (
22
TYPE_CHECKING,
3+
Awaitable,
34
Optional,
45
cast,
56
)
67

8+
from eth_utils.toolz import (
9+
curry,
10+
merge,
11+
)
12+
13+
from web3._utils.utility_methods import (
14+
any_in_dict,
15+
)
16+
from web3.constants import (
17+
DYNAMIC_FEE_TXN_PARAMS,
18+
)
719
from web3.types import (
820
BlockIdentifier,
921
TxParams,
22+
Wei,
1023
)
1124

1225
if TYPE_CHECKING:
1326
from web3 import Web3 # noqa: F401
1427
from web3.eth import AsyncEth # noqa: F401
1528

1629

30+
async def _estimate_gas(w3: 'Web3', tx: TxParams) -> Awaitable[int]:
31+
return await w3.eth.estimate_gas(tx) # type: ignore
32+
33+
34+
async def _gas_price(w3: 'Web3', tx: TxParams) -> Awaitable[Optional[Wei]]:
35+
return await w3.eth.generate_gas_price(tx) or w3.eth.gas_price # type: ignore
36+
37+
38+
async def _max_fee_per_gas(w3: 'Web3', tx: TxParams) -> Awaitable[Wei]:
39+
block = await w3.eth.get_block('latest') # type: ignore
40+
return await w3.eth.max_priority_fee + (2 * block['baseFeePerGas']) # type: ignore
41+
42+
43+
async def _max_priority_fee_gas(w3: 'Web3', tx: TxParams) -> Awaitable[Wei]:
44+
return await w3.eth.max_priority_fee # type: ignore
45+
46+
47+
async def _chain_id(w3: 'Web3', tx: TxParams) -> Awaitable[int]:
48+
return await w3.eth.chain_id # type: ignore
49+
50+
TRANSACTION_DEFAULTS = {
51+
'value': 0,
52+
'data': b'',
53+
'gas': _estimate_gas,
54+
'gasPrice': _gas_price,
55+
'maxFeePerGas': _max_fee_per_gas,
56+
'maxPriorityFeePerGas': _max_priority_fee_gas,
57+
'chainId': _chain_id,
58+
}
59+
60+
1761
async def get_block_gas_limit(
1862
web3_eth: "AsyncEth", block_identifier: Optional[BlockIdentifier] = None
1963
) -> int:
@@ -40,3 +84,38 @@ async def get_buffered_gas_estimate(
4084
)
4185

4286
return min(gas_limit, gas_estimate + gas_buffer)
87+
88+
89+
@curry
90+
async def fill_transaction_defaults(w3: "Web3", transaction: TxParams) -> TxParams:
91+
"""
92+
if w3 is None, fill as much as possible while offline
93+
"""
94+
strategy_based_gas_price = await w3.eth.generate_gas_price(transaction) # type: ignore
95+
is_dynamic_fee_transaction = (
96+
not strategy_based_gas_price
97+
and (
98+
'gasPrice' not in transaction # default to dynamic fee transaction
99+
or any_in_dict(DYNAMIC_FEE_TXN_PARAMS, transaction)
100+
)
101+
)
102+
103+
defaults = {}
104+
for key, default_getter in TRANSACTION_DEFAULTS.items():
105+
if key not in transaction:
106+
if (
107+
is_dynamic_fee_transaction and key == 'gasPrice'
108+
or not is_dynamic_fee_transaction and key in DYNAMIC_FEE_TXN_PARAMS
109+
):
110+
# do not set default max fees if legacy txn or gas price if dynamic fee txn
111+
continue
112+
113+
if callable(default_getter):
114+
if w3 is None:
115+
raise ValueError(f"You must specify a '{key}' value in the transaction")
116+
default_val = await default_getter(w3, transaction)
117+
else:
118+
default_val = default_getter
119+
120+
defaults[key] = default_val
121+
return merge(defaults, transaction)

0 commit comments

Comments
 (0)