Skip to content
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

Raise MismatchedABI exceptions with detailed error messaging #3491

Merged
merged 9 commits into from
Nov 21, 2024
18 changes: 17 additions & 1 deletion docs/web3.contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,23 @@ You can interact with the web3.py contract API as follows:
Invoke Ambiguous Contract Functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Calling overloaded functions can be done as you would expect. Passing arguments will
disambiguate which function you want to call.

For example, if you have a contract with two functions with the name ``identity`` that
accept different types of arguments, you can call them like this:

.. code-block:: python

>>> ambiguous_contract = w3.eth.contract(address=..., abi=...)
>>> ambiguous_contract.functions.identity(1, True).call()
1
>>> ambiguous_contract.functions.identity("one", 1, True).call()
1

If there is a need to first retrieve the function, you can use the contract instance's
``get_function_by_signature`` method to get the function you want to call.

Below is an example of a contract that has multiple functions of the same name,
and the arguments are ambiguous. You can use the :meth:`Contract.get_function_by_signature`
method to reference the intended function and call it with the correct arguments.
Expand All @@ -1588,7 +1605,6 @@ method to reference the intended function and call it with the correct arguments
}
"""
# fast forward all the steps of compiling and deploying the contract.
>>> ambiguous_contract.functions.identity(1, True) # raises Web3ValidationError

>>> identity_func = ambiguous_contract.get_function_by_signature('identity(uint256,bool)')
>>> identity_func(1, True)
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3491.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update the `ContractEvents` class to raise a `NoABIFound` exception if the `Contract` is initialized without an `ABI` and an attempt to access an event is made. This exception makes `ContractEvents` consistent with `ContractFunctions`.
1 change: 1 addition & 0 deletions newsfragments/3491.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Contracts with overloaded functions or events are now supported. The Contract initializes functions and events using an identifier to distinguish between them. The identifier is the function or event signature, which consists of the name and the parameter types.
1 change: 1 addition & 0 deletions newsfragments/3491.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise MismatchedABI exceptions with detailed error messaging about potential matching elements. The arguments and expected types are included in the exception message for better debugging.
60 changes: 58 additions & 2 deletions tests/core/contracts/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from tests.utils import (
async_partial,
)
from web3._utils.abi import (
get_abi_element_signature,
)
from web3._utils.contract_sources.contract_data.arrays_contract import (
ARRAYS_CONTRACT_DATA,
)
Expand All @@ -22,6 +25,7 @@
CONTRACT_CALLER_TESTER_DATA,
)
from web3._utils.contract_sources.contract_data.event_contracts import (
AMBIGUOUS_EVENT_NAME_CONTRACT_DATA,
EVENT_CONTRACT_DATA,
INDEXED_EVENT_CONTRACT_DATA,
)
Expand Down Expand Up @@ -61,6 +65,10 @@
from web3.exceptions import (
Web3ValueError,
)
from web3.utils.abi import (
abi_to_signature,
get_abi_element,
)

# --- function name tester contract --- #

Expand Down Expand Up @@ -283,6 +291,30 @@ def indexed_event_contract(
return indexed_event_contract


@pytest.fixture
def ambiguous_event_contract(
w3, wait_for_block, wait_for_transaction, address_conversion_func
):
wait_for_block(w3)

ambiguous_event_contract_factory = w3.eth.contract(
**AMBIGUOUS_EVENT_NAME_CONTRACT_DATA
)
deploy_txn_hash = ambiguous_event_contract_factory.constructor().transact(
{"gas": 1000000}
)
deploy_receipt = wait_for_transaction(w3, deploy_txn_hash)
contract_address = address_conversion_func(deploy_receipt["contractAddress"])

bytecode = w3.eth.get_code(contract_address)
assert bytecode == ambiguous_event_contract_factory.bytecode_runtime
ambiguous_event_name_contract = ambiguous_event_contract_factory(
address=contract_address
)
assert ambiguous_event_name_contract.address == contract_address
return ambiguous_event_name_contract


# --- arrays contract --- #


Expand Down Expand Up @@ -470,6 +502,11 @@ def invoke_contract(
func_kwargs=None,
tx_params=None,
):
function_signature = contract_function
function_arg_count = len(func_args or ()) + len(func_kwargs or {})
if function_arg_count == 0:
function_signature = get_abi_element_signature(contract_function)

if func_args is None:
func_args = []
if func_kwargs is None:
Expand All @@ -482,7 +519,14 @@ def invoke_contract(
f"allowable_invoke_method must be one of: {allowable_call_desig}"
)

function = contract.functions[contract_function]
fn_abi = get_abi_element(
contract.abi,
function_signature,
*func_args,
abi_codec=contract.w3.codec,
**func_kwargs,
)
function = contract.functions[abi_to_signature(fn_abi)]
result = getattr(function(*func_args, **func_kwargs), api_call_desig)(tx_params)

return result
Expand Down Expand Up @@ -744,6 +788,11 @@ async def async_invoke_contract(
func_kwargs=None,
tx_params=None,
):
function_signature = contract_function
function_arg_count = len(func_args or ()) + len(func_kwargs or {})
if function_arg_count == 0:
function_signature = get_abi_element_signature(contract_function)

if func_args is None:
func_args = []
if func_kwargs is None:
Expand All @@ -756,7 +805,14 @@ async def async_invoke_contract(
f"allowable_invoke_method must be one of: {allowable_call_desig}"
)

function = contract.functions[contract_function]
fn_abi = get_abi_element(
contract.abi,
function_signature,
*func_args,
abi_codec=contract.w3.codec,
**func_kwargs,
)
function = contract.functions[abi_to_signature(fn_abi)]
result = await getattr(function(*func_args, **func_kwargs), api_call_desig)(
tx_params
)
Expand Down
Loading