Skip to content

Commit 34d258f

Browse files
fselmoreedsa
andauthored
Fix initialization of Contract function and events (#3540)
* Do not invoke ``ens`` until it is actually used: - ``web3.ens`` invokes the ``@property`` getter on ``ens``. This getter sets ``self._ens`` on the ``web3`` object if it is empty. We do not need to do invoke this getter until ``ens`` is actually called. Instead, use the pointer to ``self._ens`` directly to avoid overhead when the majority of uses may not even be using ENS. * DRY contract function instantiation: - When initializing a contract, we create the ``ContractFunction`` objects for each function and assign them to the ``contract.functions`` attribute. Then we instantiate the ``contract.caller`` and inside the ``ContractCaller`` init, we re-instantiate each contract function. Pass these around instead which will prevent so many calls for validation. They are already built, so there is no reason to build them again. * Async changes related to 999ef9a * Silence some warnings on wrong type expectations. * Try using self inside ContractFunction __call__(): - Instead of re-building the ``ContractFunction`` object, use the already existing ``self`` object reference. * Remove seting `abi` on the clone since it already contains it. * Call contract function using the instance properly to prevent ambiguous errors. Add fixture contract with ambiguous functions and arguments. * ContractFuncion `__call__` now uses self unless it does not match the arguments. Initialize ContractFunction and ContractEvent with `signature`. Create ContractFunctions with properties for the function name and also private properties with each function signature Create ContractEvents with properties for the event name and also private properties with each event signature * Use built contract functions for async contract caller init; add tests - Pass the already built contract functions in the contract init when initializing the contract caller for ``AsyncContract``. - Add reasonable init time test for ``Contract`` and ``AsyncContract`` so that we know when code deviates from the expected performance. * Corrections based on feedback * Document functions and events for latest changes. Include comments for overloaded functions with similar arguments. * Newsfragment for #3540 --------- Co-authored-by: Stuart Reed <[email protected]>
1 parent f6204f1 commit 34d258f

17 files changed

+915
-308
lines changed

docs/web3.contract.rst

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ Each Contract Factory exposes the following methods.
463463
.. doctest:: contractmethods
464464

465465
>>> contract.all_functions()
466-
[<Function name()>, <Function approve(address,uint256)>, <Function totalSupply()>, <Function transferFrom(address,address,uint256)>, <Function decimals()>, <Function balanceOf(address)>, <Function symbol()>, <Function transfer(address,uint256)>, <Function allowance(address,address)>]
466+
[<Function allowance(address,address)>, <Function approve(address,uint256)>, <Function balanceOf(address)>, <Function decimals()>, <Function name()>, <Function symbol()>, <Function totalSupply()>, <Function transfer(address,uint256)>, <Function transferFrom(address,address,uint256)>]
467467

468468

469469
.. py:classmethod:: Contract.get_function_by_signature(signature)
@@ -590,7 +590,6 @@ size, web3 will invalidate the value. For example, if an abi specifies a type of
590590
* - ``'0x6162636464'``
591591
- Needs to have exactly 4 bytes
592592

593-
594593
However, you may want to be less strict with acceptable values for bytes types.
595594
This may prove useful if you trust that values coming through are what they are
596595
meant to be with respect to the ABI. In this case, the automatic padding might be
@@ -778,8 +777,6 @@ Taking the following contract code as an example:
778777
Contract Functions
779778
------------------
780779

781-
.. py:class:: ContractFunction
782-
783780
The named functions exposed through the :py:attr:`Contract.functions` property are
784781
of the ContractFunction type. This class is not to be used directly,
785782
but instead through :py:attr:`Contract.functions`.
@@ -809,6 +806,69 @@ take any arguments. For example:
809806
>>> myContract.functions.return13().call()
810807
13
811808
809+
In cases where functions are overloaded and use arguments of similar types, a function
810+
may resolve to an undesired function when using the method syntax. For example, given
811+
two functions with the same name that take a single argument of differing types of
812+
``bytes`` and ``bytes32``. When a reference to :meth:`contract.functions.setBytes(b'1')`
813+
is used, the function will resolve as the one that takes ``bytes``. If a value is passed
814+
that was meant to be 32 bytes but the given argument was off by one, the function
815+
reference will still use the one that takes ``bytes``.
816+
817+
When in doubt, use explicit function references to the signature. Use bracket notation
818+
(:meth:`contract.functions["setBytes(bytes32)"](b'')`) or the contract API
819+
`contract.get_function_by_signature("setBytes(bytes32)")` to retrieve the desired
820+
function. This will ensure an exception is raised if the argument is not strictly
821+
32 bytes in length.
822+
823+
.. py:class:: ContractFunction
824+
825+
Attributes
826+
~~~~~~~~~~
827+
828+
The :py:class:`ContractFunction` class provides attributes for each function. Access the function attributes through `Contract.functions.myMethod`.
829+
830+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).abi_element_identifier
831+
832+
The signature of the function assigned to the class ``__name__`` during initialization.
833+
Fallback and Receive functions will be assigned as classes :py:class:`FallbackFn` or
834+
:py:class:`RecieveFn` respectively.
835+
836+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).name
837+
838+
A string representing the function, receive or fallback name.
839+
840+
Use :py:attr:`ContractFunction.signature` when the function arguments are needed.
841+
842+
This is an alias of :py:attr:`ContractFunction.fn_name`.
843+
844+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).signature
845+
846+
A string representing the function, receive or fallback signature.
847+
848+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).selector
849+
850+
A HexStr encoded from the first four bytes of the function signature.
851+
852+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).abi
853+
854+
The function ABI with the type, name, inputs and outputs.
855+
856+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).arguments
857+
858+
A tuple of all function inputs, normalized so that `kwargs` themselves are
859+
flattened into a tuple as returned by :py:meth:`eth_utils.abi.get_normalized_abi_inputs`.
860+
861+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).argument_names
862+
863+
The function input names.
864+
865+
.. py:attribute:: ContractFunction.myMethod(*args, **kwargs).argument_types
866+
867+
The function input types.
868+
869+
Methods
870+
~~~~~~~
871+
812872
:py:class:`ContractFunction` provides methods to interact with contract functions.
813873
Positional and keyword arguments supplied to the contract function subclass
814874
will be used to find the contract function by signature,
@@ -847,10 +907,6 @@ message that comes back and return it to the user as the error string.
847907
As of v6.3.0, the raw data is also returned and
848908
can be accessed via the ``data`` attribute on ``ContractLogicError``.
849909

850-
851-
Methods
852-
~~~~~~~
853-
854910
.. py:method:: ContractFunction.transact(transaction)
855911
856912
Execute the specified function by sending a new public transaction.
@@ -1040,12 +1096,10 @@ Fallback Function
10401096
10411097
Builds a transaction dictionary based on the contract fallback function call.
10421098

1043-
Events
1044-
------
1045-
1046-
.. py:class:: ContractEvents
1099+
Contract Events
1100+
---------------
10471101

1048-
The named events exposed through the :py:attr:`Contract.events` property are of the ContractEvents type. This class is not to be used directly, but instead through :py:attr:`Contract.events`.
1102+
The named events exposed through the :py:attr:`Contract.events` property are of the ContractEvent type. This class is not to be used directly, but instead through :py:attr:`Contract.events`.
10491103

10501104
For example:
10511105

@@ -1056,11 +1110,49 @@ For example:
10561110
receipt = web3.eth.get_transaction_receipt(tx_hash)
10571111
myContract.events.myEvent().process_receipt(receipt)
10581112
1113+
.. py:class:: ContractEvent
1114+
1115+
Attributes
1116+
~~~~~~~~~~
1117+
1118+
The :py:class:`ContractEvent` class provides attributes for each event. Access the event attributes through `Contract.events.myEvent`.
1119+
1120+
.. py:attribute:: ContractEvent.myEvent(*args, **kwargs).abi_element_identifier
1121+
1122+
The signature of the event assigned to the class ``__name__`` during initialization.
1123+
1124+
.. py:attribute:: ContractEvent.myEvent(*args, **kwargs).name
1125+
1126+
A string representing the event, receive or fallback name.
1127+
1128+
Use :py:attr:`ContractEvent.myEvent(*args, **kwargs).signature` when the event arguments are needed.
1129+
1130+
This is an alias of :py:attr:`ContractEvent.myEvent(*args, **kwargs).event_name`.
1131+
1132+
.. py:attribute:: ContractEvent.myEvent(*args, **kwargs).signature
1133+
1134+
A string representing the event signature.
1135+
1136+
.. py:attribute:: ContractEvent.myEvent(*args, **kwargs).abi
1137+
1138+
The event ABI with the type, name, inputs.
1139+
1140+
.. py:attribute:: ContractEvent.myEvent(*args, **kwargs).argument_names
1141+
1142+
The event input names.
1143+
1144+
.. py:attribute:: ContractEvent.myEvent(*args, **kwargs).argument_types
1145+
1146+
The event input types.
1147+
1148+
Methods
1149+
~~~~~~~
1150+
10591151
:py:class:`ContractEvent` provides methods to interact with contract events. Positional and keyword arguments supplied to the contract event subclass will be used to find the contract event by signature.
10601152

10611153
.. _contract_get_logs:
10621154

1063-
.. py:method:: ContractEvents.myEvent(*args, **kwargs).get_logs(from_block=None, to_block="latest", block_hash=None, argument_filters={})
1155+
.. py:method:: ContractEvent.get_logs(from_block=None, to_block="latest", block_hash=None, argument_filters={})
10641156
:noindex:
10651157

10661158
Fetches all logs for a given event within the specified block range or block hash.
@@ -1090,7 +1182,7 @@ For example:
10901182
10911183
.. _process_receipt:
10921184

1093-
.. py:method:: ContractEvents.myEvent(*args, **kwargs).process_receipt(transaction_receipt, errors=WARN)
1185+
.. py:method:: ContractEvent.process_receipt(transaction_receipt, errors=WARN)
10941186
:noindex:
10951187

10961188
Extracts the pertinent logs from a transaction receipt.
@@ -1159,7 +1251,7 @@ For example:
11591251
>>> assert processed_logs == ()
11601252
True
11611253
1162-
.. py:method:: ContractEvents.myEvent(*args, **kwargs).process_log(log)
1254+
.. py:method:: ContractEvent.process_log(log)
11631255
11641256
Similar to process_receipt_, but only processes one log at a time, instead of a whole transaction receipt.
11651257
Will return a single :ref:`Event Log Object <event-log-object>` if there are no errors encountered during processing. If an error is encountered during processing, it will be raised.

newsfragments/3540.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Contract functions and events no longer initialize for each call. Retrieval of overloaded functions and events is now deterministic. Any ambiguity will result in an exception being raised.

tests/core/contracts/conftest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from web3._utils.abi import (
1414
get_abi_element_signature,
1515
)
16+
from web3._utils.contract_sources.contract_data.ambiguous_function_contract import (
17+
AMBIGUOUS_FUNCTION_CONTRACT_DATA,
18+
)
1619
from web3._utils.contract_sources.contract_data.arrays_contract import (
1720
ARRAYS_CONTRACT_DATA,
1821
)
@@ -195,6 +198,26 @@ def non_strict_string_contract(
195198
)
196199

197200

201+
# --- ambiguous function contract --- #
202+
203+
204+
@pytest.fixture(scope="session")
205+
def ambiguous_function_contract_data():
206+
return AMBIGUOUS_FUNCTION_CONTRACT_DATA
207+
208+
209+
@pytest.fixture
210+
def ambiguous_function_contract_factory(w3):
211+
return w3.eth.contract(**AMBIGUOUS_FUNCTION_CONTRACT_DATA)
212+
213+
214+
@pytest.fixture
215+
def ambiguous_function_contract(
216+
w3, ambiguous_function_contract_factory, address_conversion_func
217+
):
218+
return deploy(w3, ambiguous_function_contract_factory, address_conversion_func)
219+
220+
198221
# --- emitter contract --- #
199222

200223

@@ -780,6 +803,17 @@ async def async_nested_tuple_contract_with_decode_tuples(
780803
)
781804

782805

806+
@pytest_asyncio.fixture
807+
async def async_ambiguous_event_contract(async_w3, address_conversion_func):
808+
async_ambiguous_event_contract_factory = async_w3.eth.contract(
809+
**AMBIGUOUS_EVENT_NAME_CONTRACT_DATA
810+
)
811+
812+
return await async_deploy(
813+
async_w3, async_ambiguous_event_contract_factory, address_conversion_func
814+
)
815+
816+
783817
async def async_invoke_contract(
784818
api_call_desig="call",
785819
contract=None,

0 commit comments

Comments
 (0)