From f89f3794d7022981221c3a607b72cb61df63bc7d Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Thu, 24 Oct 2024 14:06:35 -0600 Subject: [PATCH 1/8] Overloaded functions with better MismatchedABI messaging --- docs/web3.contract.rst | 18 +- newsfragments/3491.bugfix.rst | 1 + newsfragments/3491.feature.rst | 1 + newsfragments/3491.misc.rst | 1 + tests/core/contracts/conftest.py | 60 ++- .../test_contract_ambiguous_events.py | 259 ++++++++++++ .../test_contract_ambiguous_functions.py | 6 + .../contracts/test_contract_attributes.py | 5 + .../contracts/test_contract_call_interface.py | 299 ++++++++++---- .../test_contract_class_construction.py | 112 +++++ ...st_contract_method_to_argument_matching.py | 60 ++- tests/core/utilities/test_abi.py | 196 ++++++++- tests/core/utilities/test_event_interface.py | 3 +- tests/core/utilities/test_validation.py | 1 + web3/_utils/abi.py | 62 ++- .../contract_sources/EventContracts.sol | 10 + .../contract_data/event_contracts.py | 95 +---- .../function_name_tester_contract.py | 25 +- web3/_utils/contracts.py | 55 ++- web3/_utils/validation.py | 3 + web3/contract/async_contract.py | 161 +++++--- web3/contract/base_contract.py | 259 ++++++------ web3/contract/contract.py | 151 ++++--- web3/contract/utils.py | 5 +- web3/utils/abi.py | 381 ++++++++++++++---- 25 files changed, 1743 insertions(+), 486 deletions(-) create mode 100644 newsfragments/3491.bugfix.rst create mode 100644 newsfragments/3491.feature.rst create mode 100644 newsfragments/3491.misc.rst create mode 100644 tests/core/contracts/test_contract_ambiguous_events.py diff --git a/docs/web3.contract.rst b/docs/web3.contract.rst index d61db0e285..b3a936cc6b 100644 --- a/docs/web3.contract.rst +++ b/docs/web3.contract.rst @@ -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. @@ -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) diff --git a/newsfragments/3491.bugfix.rst b/newsfragments/3491.bugfix.rst new file mode 100644 index 0000000000..b2a5ad4b18 --- /dev/null +++ b/newsfragments/3491.bugfix.rst @@ -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`. diff --git a/newsfragments/3491.feature.rst b/newsfragments/3491.feature.rst new file mode 100644 index 0000000000..df6e0a81d9 --- /dev/null +++ b/newsfragments/3491.feature.rst @@ -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. diff --git a/newsfragments/3491.misc.rst b/newsfragments/3491.misc.rst new file mode 100644 index 0000000000..20b637b96b --- /dev/null +++ b/newsfragments/3491.misc.rst @@ -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. diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index 42e4a09080..b164d90cbf 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -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, ) @@ -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, ) @@ -61,6 +65,10 @@ from web3.exceptions import ( Web3ValueError, ) +from web3.utils.abi import ( + abi_to_signature, + get_abi_element, +) # --- function name tester contract --- # @@ -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 --- # @@ -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: @@ -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 @@ -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: @@ -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 ) diff --git a/tests/core/contracts/test_contract_ambiguous_events.py b/tests/core/contracts/test_contract_ambiguous_events.py new file mode 100644 index 0000000000..2a79a339b2 --- /dev/null +++ b/tests/core/contracts/test_contract_ambiguous_events.py @@ -0,0 +1,259 @@ +import pytest +from typing import ( + cast, +) + +from eth_typing import ( + ABI, +) + +from web3.contract.contract import ( + Contract, + ContractEvent, +) +from web3.exceptions import ( + MismatchedABI, +) +from web3.main import ( + Web3, +) +from web3.utils.abi import ( + get_abi_element, +) + +ABI_EVENT_TRANSFER = { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "indexed": True, + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "value", + "type": "uint256", + }, + ], + "name": "Transfer", + "type": "event", +} +ABI_EVENT_DEPOSIT_WITH_TUPLE = { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "indexed": True, + "internalType": "bytes32", + "name": "id", + "type": "bytes32", + }, + { + "components": [ + {"internalType": "uint256", "name": "x", "type": "uint256"}, + {"internalType": "uint256", "name": "y", "type": "uint256"}, + ], + "indexed": False, + "internalType": "struct DepositEventContract.T", + "name": "value", + "type": "tuple", + }, + ], + "name": "Deposit", + "type": "event", +} + +ABI_EVENT_DEPOSIT = { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "indexed": True, + "internalType": "bytes32", + "name": "id", + "type": "bytes32", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "value", + "type": "uint256", + }, + ], + "name": "Deposit", + "type": "event", +} +ABI_FUNCTION_DEPOSIT_STRUCT = { + "inputs": [{"internalType": "bytes32", "name": "id", "type": "bytes32"}], + "name": "depositStruct", + "outputs": [], + "stateMutability": "payable", + "type": "function", +} +ABI_FUNCTION_DEPOSIT_VALUE = { + "inputs": [{"internalType": "bytes32", "name": "id", "type": "bytes32"}], + "name": "depositValue", + "outputs": [], + "stateMutability": "payable", + "type": "function", +} +AMBIGUOUS_EVENT_WITH_TUPLE_CONTRACT_ABI = [ + ABI_EVENT_DEPOSIT_WITH_TUPLE, + ABI_EVENT_DEPOSIT, + ABI_EVENT_TRANSFER, + ABI_FUNCTION_DEPOSIT_STRUCT, + ABI_FUNCTION_DEPOSIT_VALUE, +] + + +def test_get_abi_from_event(ambiguous_event_contract: "Contract") -> None: + expected_event_abi = { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleArg", + "type": "event", + } + + event_get_abi_result = ambiguous_event_contract.events["LogSingleArg(uint256)"].abi + assert expected_event_abi == event_get_abi_result + + +def test_get_abi_element_for_ambiguous_tuple_events() -> None: + event_abi = get_abi_element( + cast(ABI, AMBIGUOUS_EVENT_WITH_TUPLE_CONTRACT_ABI), + "Deposit", + *[ + "0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7", + b"0x0", + ], + **{"value": {"x": 1, "y": 2}}, + ) + + assert event_abi == ABI_EVENT_DEPOSIT_WITH_TUPLE + + event_abi = get_abi_element( + cast(ABI, AMBIGUOUS_EVENT_WITH_TUPLE_CONTRACT_ABI), + "Deposit", + *[ + "0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7", + b"0x0", + 1, + ], + ) + + assert event_abi == ABI_EVENT_DEPOSIT + + +def test_get_abi_element_with_signature_for_ambiguous_tuple_events() -> None: + event_abi = get_abi_element( + cast(ABI, AMBIGUOUS_EVENT_WITH_TUPLE_CONTRACT_ABI), + "Deposit(address,bytes32,(uint256,uint256))", + ) + + assert event_abi == ABI_EVENT_DEPOSIT_WITH_TUPLE + + event_abi = get_abi_element( + cast(ABI, AMBIGUOUS_EVENT_WITH_TUPLE_CONTRACT_ABI), + "Deposit(address,bytes32,uint256)", + ) + + assert event_abi == ABI_EVENT_DEPOSIT + + +def test_get_abi_element_by_name_and_arguments_errors( + ambiguous_event_contract: "Contract", +) -> None: + with pytest.raises( + MismatchedABI, + match=r"ABI Not Found!\nNo element named `NotAnEvent` with 0 argument\(s\).", + ): + get_abi_element(ambiguous_event_contract.abi, "NotAnEvent") + + +def test_contract_event_methods( + w3: "Web3", ambiguous_event_contract: "Contract" +) -> None: + log_arg_event = cast( + ContractEvent, ambiguous_event_contract.events["LogSingleArg(uint256)"] + ) + + assert log_arg_event.abi == { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleArg", + "type": "event", + } + assert log_arg_event.event_name == "LogSingleArg" + assert log_arg_event.abi_element_identifier == "LogSingleArg(uint256)" + assert log_arg_event.get_logs() == [] + + filter_builder = log_arg_event.build_filter() + filter_builder.from_block = "latest" + filter_builder.to_block = "latest" + filter_builder.args.arg0.match_single(1) + filter_instance = filter_builder.deploy(w3) + + assert filter_instance.filter_params == { + "fromBlock": "latest", + "toBlock": "latest", + "address": ambiguous_event_contract.address, + "topics": ( + "0x56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4", + ), + } + + log_filter = log_arg_event.create_filter(from_block="latest") + log_entry = { + "address": ambiguous_event_contract.address, + "topics": ( + "0x56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4", + ), + "data": "0x0000000000000000000000000000000000000000000000000000000000000005", + "logIndex": 0, + "transactionIndex": 0, + "transactionHash": "0x0", + "blockHash": "0x0", + "blockNumber": 0, + } + assert log_filter.log_entry_formatter(log_entry) == { + "args": {"arg0": 5}, + "event": "LogSingleArg", + "logIndex": 0, + "transactionIndex": 0, + "transactionHash": "0x0", + "address": ambiguous_event_contract.address, + "blockHash": "0x0", + "blockNumber": 0, + } diff --git a/tests/core/contracts/test_contract_ambiguous_functions.py b/tests/core/contracts/test_contract_ambiguous_functions.py index 047343e3ea..42c5dcc878 100644 --- a/tests/core/contracts/test_contract_ambiguous_functions.py +++ b/tests/core/contracts/test_contract_ambiguous_functions.py @@ -83,6 +83,12 @@ def string_contract(w3, string_contract_factory, address_conversion_func): repr, "", ), + ( + "get_function_by_signature", + ("identity(int256,bool)",), + repr, + "", + ), ( "find_functions_by_name", ("identity",), diff --git a/tests/core/contracts/test_contract_attributes.py b/tests/core/contracts/test_contract_attributes.py index bb36550f48..53fc296229 100644 --- a/tests/core/contracts/test_contract_attributes.py +++ b/tests/core/contracts/test_contract_attributes.py @@ -11,6 +11,11 @@ def abi(): return """[{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"function"}, {"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"event"}]""" # noqa: E501 +@pytest.fixture() +def ambiguous_abis(): + return """[{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"function"}, {"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"bytes32"}],"name":"Increased","type":"function"}, {"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"event"}, {"anonymous":false,"inputs":[],"name":"Increased","type":"event"}]""" # noqa: E501 + + @pytest.mark.parametrize("attribute", ("functions", "events", "caller")) def test_getattr(w3, abi, attribute): contract = w3.eth.contract(abi=abi) diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index 7b3e935009..9f3ca8b906 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -4,6 +4,7 @@ ) import json import pytest +import re from eth_tester.exceptions import ( TransactionFailed, @@ -276,7 +277,7 @@ def test_set_byte_array_non_strict( def test_set_byte_array_with_invalid_args(arrays_contract, transact, args): with pytest.raises( MismatchedABI, - match="Could not identify the intended function with name `setByteValue`", + match=r"Found 1 element\(s\) named `setByteValue` that accept 1 argument\(s\).\n", # noqa: E501 ): transact( contract=arrays_contract, @@ -597,52 +598,120 @@ def test_returns_data_from_specified_block(w3, math_contract): assert output2 == 2 -message_regex = ( - r"\nCould not identify the intended function with name `.*`, positional arguments " - r"with type\(s\) `.*` and keyword arguments with type\(s\) `.*`." - r"\nFound .* function\(s\) with the name `.*`: .*" -) -diagnosis_arg_regex = ( - r"\nFunction invocation failed due to improper number of arguments." -) -diagnosis_encoding_regex = ( - r"\nFunction invocation failed due to no matching argument types." -) -diagnosis_ambiguous_encoding = ( - r"\nAmbiguous argument encoding. " - r"Provided arguments can be encoded to multiple functions matching this call." -) - - def test_no_functions_match_identifier(arrays_contract): with pytest.raises(MismatchedABI): arrays_contract.functions.thisFunctionDoesNotExist().call() def test_function_1_match_identifier_wrong_number_of_args(arrays_contract): - regex = message_regex + diagnosis_arg_regex - with pytest.raises(MismatchedABI, match=regex): + with pytest.raises( + MismatchedABI, + match=re.escape( + "\nABI Not Found!\n" + "No element named `setBytes32Value` with 0 argument(s).\n" + "Provided argument types: ()\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setBytes32Value`, but " + "encountered the following problems:\n" + "Signature: setBytes32Value(bytes32[]), type: function\n" + "Expected 1 argument(s) but received 0 argument(s).\n" + ), + ): arrays_contract.functions.setBytes32Value().call() def test_function_1_match_identifier_wrong_args_encoding(arrays_contract): - regex = message_regex + diagnosis_encoding_regex - with pytest.raises(MismatchedABI, match=regex): + with pytest.raises( + MismatchedABI, + match=re.escape( + "\nABI Not Found!\n" + "Found 1 element(s) named `setBytes32Value` that accept 1 argument(s).\n" + "The provided arguments are not valid.\n" + "Provided argument types: (str)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setBytes32Value`, but " + "encountered the following problems:\n" + "Signature: setBytes32Value(bytes32[]), type: function\n" + "Argument 1 value `dog` is not compatible with type `bytes32[]`.\n" + ), + ): arrays_contract.functions.setBytes32Value("dog").call() @pytest.mark.parametrize( - "arg1,arg2,diagnosis", + "arg1,arg2,message", ( - (100, "dog", diagnosis_arg_regex), - ("dog", None, diagnosis_encoding_regex), - (100, None, diagnosis_ambiguous_encoding), + ( + 100, + "dog", + ( + "\nABI Not Found!\n" + "No element named `a` with 2 argument(s).\n" + "Provided argument types: (int,str)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `a`, but encountered the " + "following problems:\n" + "Signature: a(), type: function\n" + "Expected 0 argument(s) but received 2 argument(s).\n" + "Signature: a(bytes32), type: function\n" + "Expected 1 argument(s) but received 2 argument(s).\n" + "Signature: a(uint256), type: function\n" + "Expected 1 argument(s) but received 2 argument(s).\n" + "Signature: a(uint8), type: function\n" + "Expected 1 argument(s) but received 2 argument(s).\n" + "Signature: a(int8), type: function\n" + "Expected 1 argument(s) but received 2 argument(s).\n" + ), + ), + ( + "dog", + None, + ( + "\nABI Not Found!\n" + "Found multiple elements named `a` that accept 1 argument(s).\n" + "Provided argument types: (str)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `a`, but encountered the " + "following problems:\n" + "Signature: a(bytes32), type: function\n" + "Argument 1 value `dog` is not compatible with type `bytes32`.\n" + "Signature: a(uint256), type: function\n" + "Argument 1 value `dog` is not compatible with type `uint256`.\n" + "Signature: a(uint8), type: function\n" + "Argument 1 value `dog` is not compatible with type `uint8`.\n" + "Signature: a(int8), type: function\n" + "Argument 1 value `dog` is not compatible with type `int8`.\n" + "Signature: a(), type: function\n" + "Expected 0 argument(s) but received 1 argument(s).\n" + ), + ), + ( + 100, + None, + ( + "\nABI Not Found!\n" + "Found multiple elements named `a` that accept 1 argument(s).\n" + "Provided argument types: (int)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `a`, but encountered the " + "following problems:\n" + "Signature: a(bytes32), type: function\n" + "Argument 1 value `100` is not compatible with type `bytes32`.\n" + "Signature: a(uint256), type: function\n" + "Argument 1 value `100` is valid.\n" + "Signature: a(uint8), type: function\n" + "Argument 1 value `100` is valid.\n" + "Signature: a(int8), type: function\n" + "Argument 1 value `100` is valid.\n" + "Signature: a(), type: function\n" + "Expected 0 argument(s) but received 1 argument(s).\n" + ), + ), ), ) -def test_function_multiple_error_diagnoses(w3, arg1, arg2, diagnosis): +def test_function_multiple_error_diagnoses(w3, arg1, arg2, message): Contract = w3.eth.contract(abi=MULTIPLE_FUNCTIONS) - regex = message_regex + diagnosis - with pytest.raises(MismatchedABI, match=regex): + with pytest.raises(MismatchedABI, match=re.escape(message)): if arg2: Contract.functions.a(arg1, arg2).call() else: @@ -672,9 +741,9 @@ def test_function_wrong_args_for_tuple_collapses_args_in_message( ) # assert the found method signature is formatted as expected: - # ['method((uint256,uint256[],(int256,bool[2],address[])[]))'] + # 'method((uint256,uint256[],(int256,bool[2],address[])[]))' e.match( - "\\['method\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)'\\]" # noqa: E501 + "\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)" # noqa: E501 ) @@ -702,7 +771,7 @@ def test_function_wrong_args_for_tuple_collapses_kwargs_in_message( # assert the found method signature is formatted as expected: # ['method((uint256,uint256[],(int256,bool[2],address[])[]))'] e.match( - "\\['method\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)'\\]" # noqa: E501 + "\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)" # noqa: E501 ) @@ -736,16 +805,19 @@ def test_call_sending_ether_to_nonpayable_function(payable_tester_contract, call "function, value", ( # minimum positive unambiguous value (larger than fixed8x1) - ("reflect", Decimal("12.8")), + ("reflect(fixed8x1)", Decimal("12.8")), # maximum value (for ufixed256x1) - ("reflect", Decimal(2**256 - 1) / 10), + ("reflect(ufixed256x1)", Decimal(2**256 - 1) / 10), # maximum negative unambiguous value (less than 0 from ufixed*) - ("reflect", Decimal("-0.1")), + ("reflect(ufixed256x1)", Decimal("-0.1")), # minimum value (for fixed8x1) - ("reflect", Decimal("-12.8")), + ("reflect(fixed8x1)", Decimal("-12.8")), # only ufixed256x80 type supports 2-80 decimals - ("reflect", Decimal(2**256 - 1) / 10**80), # maximum allowed value - ("reflect", Decimal(1) / 10**80), # smallest non-zero value + ( + "reflect(ufixed256x80)", + Decimal(2**256 - 1) / 10**80, + ), # maximum allowed value + ("reflect(ufixed256x80)", Decimal(1) / 10**80), # smallest non-zero value # minimum value (for ufixed8x1) ("reflect_short_u", 0), # maximum value (for ufixed8x1) @@ -765,29 +837,65 @@ def test_reflect_fixed_value(fixed_reflector_contract, function, value): "function, value, error", ( # out of range - ("reflect_short_u", Decimal("25.6"), "no matching argument types"), - ("reflect_short_u", Decimal("-.1"), "no matching argument types"), + ( + "reflect_short_u", + Decimal("25.6"), + "Argument 1 value `25.6` is not compatible with type `ufixed8x1`.", + ), + ( + "reflect_short_u", + Decimal("-.1"), + "Argument 1 value `-0.1` is not compatible with type `ufixed8x1`.", + ), # too many digits for *x1, too large for 256x80 - ("reflect", Decimal("0.01"), "no matching argument types"), + ( + "reflect(ufixed256x80)", + Decimal("0.01"), + "Argument 1 value `0.01` is not compatible with type `ufixed256x80`.", + ), # too many digits - ("reflect_short_u", Decimal("0.01"), "no matching argument types"), + ( + "reflect_short_u", + Decimal("0.01"), + "Argument 1 value `0.01` is not compatible with type `ufixed8x1`.", + ), ( "reflect_short_u", Decimal(f"1e-{DEFAULT_DECIMALS + 1}"), - "no matching argument types", + "Argument 1 value `1E-29` is not compatible with type `ufixed8x1`.", ), ( "reflect_short_u", Decimal("25.4" + "9" * DEFAULT_DECIMALS), - "no matching argument types", + "Argument 1 value `25.49999999999999999999999999999` is not compatible with type `ufixed8x1`.", # noqa: E501 + ), + ( + "reflect(ufixed256x80)", + Decimal(1) / 10**81, + "Argument 1 value `1E-81` is not compatible with type `ufixed256x80`.", ), - ("reflect", Decimal(1) / 10**81, "no matching argument types"), # floats not accepted, for floating point error concerns - ("reflect_short_u", 0.1, "no matching argument types"), + ( + "reflect_short_u", + 0.1, + "Argument 1 value `0.1` is not compatible with type `ufixed8x1`.", + ), # ambiguous - ("reflect", Decimal("12.7"), "Ambiguous argument encoding"), - ("reflect", Decimal(0), "Ambiguous argument encoding"), - ("reflect", 0, "Ambiguous argument encoding"), + ( + "reflect(ufixed256x80)", + Decimal("12.7"), + r"Found multiple elements named `reflect` that accept 1 argument\(s\).", + ), + ( + "reflect(ufixed256x80)", + Decimal(0), + r"Found multiple elements named `reflect` that accept 1 argument\(s\).", + ), + ( + "reflect(ufixed256x80)", + 0, + r"Found multiple elements named `reflect` that accept 1 argument\(s\).", + ), ), ) def test_invalid_fixed_value_reflections( @@ -1789,8 +1897,18 @@ async def test_async_no_functions_match_identifier(async_arrays_contract): async def test_async_function_1_match_identifier_wrong_number_of_args( async_arrays_contract, ): - regex = message_regex + diagnosis_arg_regex - with pytest.raises(MismatchedABI, match=regex): + with pytest.raises( + MismatchedABI, + match=re.escape( + "No element named `setBytes32Value` with 0 argument(s).\n" + "Provided argument types: ()\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setBytes32Value`, but " + "encountered the following problems:\n" + "Signature: setBytes32Value(bytes32[]), type: function\n" + "Expected 1 argument(s) but received 0 argument(s).\n" + ), + ): await async_arrays_contract.functions.setBytes32Value().call() @@ -1798,24 +1916,43 @@ async def test_async_function_1_match_identifier_wrong_number_of_args( async def test_async_function_1_match_identifier_wrong_args_encoding( async_arrays_contract, ): - regex = message_regex + diagnosis_encoding_regex - with pytest.raises(MismatchedABI, match=regex): + with pytest.raises( + MismatchedABI, + match=re.escape( + "\nABI Not Found!\n" + "Found 1 element(s) named `setBytes32Value` that accept 1 argument(s).\n" + "The provided arguments are not valid.\n" + "Provided argument types: (str)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setBytes32Value`, but " + "encountered the following problems:\n" + "Signature: setBytes32Value(bytes32[]), type: function\n" + "Argument 1 value `dog` is not compatible with type `bytes32[]`.\n" + ), + ): await async_arrays_contract.functions.setBytes32Value("dog").call() @pytest.mark.asyncio @pytest.mark.parametrize( - "arg1,arg2,diagnosis", + "arg1,arg2,message", ( - (100, "dog", diagnosis_arg_regex), - ("dog", None, diagnosis_encoding_regex), - (100, None, diagnosis_ambiguous_encoding), + (100, "dog", "No element named `a` with 2 argument(s)."), + ( + "dog", + None, + "Found multiple elements named `a` that accept 1 argument(s).", + ), + ( + 100, + None, + "Found multiple elements named `a` that accept 1 argument(s).", + ), ), ) -async def test_async_function_multiple_error_diagnoses(async_w3, arg1, arg2, diagnosis): +async def test_async_function_multiple_error_diagnoses(async_w3, arg1, arg2, message): Contract = async_w3.eth.contract(abi=MULTIPLE_FUNCTIONS) - regex = message_regex + diagnosis - with pytest.raises(MismatchedABI, match=regex): + with pytest.raises(MismatchedABI, match=re.escape(message)): if arg2: await Contract.functions.a(arg1, arg2).call() else: @@ -1863,16 +2000,19 @@ async def test_async_call_sending_ether_to_nonpayable_function( "function, value", ( # minimum positive unambiguous value (larger than fixed8x1) - ("reflect", Decimal("12.8")), + ("reflect(fixed8x1)", Decimal("12.8")), # maximum value (for ufixed256x1) - ("reflect", Decimal(2**256 - 1) / 10), + ("reflect(ufixed256x1)", Decimal(2**256 - 1) / 10), # maximum negative unambiguous value (less than 0 from ufixed*) - ("reflect", Decimal("-0.1")), + ("reflect(ufixed256x1)", Decimal("-0.1")), # minimum value (for fixed8x1) - ("reflect", Decimal("-12.8")), + ("reflect(fixed8x1)", Decimal("-12.8")), # only ufixed256x80 type supports 2-80 decimals - ("reflect", Decimal(2**256 - 1) / 10**80), # maximum allowed value - ("reflect", Decimal(1) / 10**80), # smallest non-zero value + ( + "reflect(ufixed256x80)", + Decimal(2**256 - 1) / 10**80, + ), # maximum allowed value + ("reflect(ufixed256x80)", Decimal(1) / 10**80), # smallest non-zero value # minimum value (for ufixed8x1) ("reflect_short_u", 0), # maximum value (for ufixed8x1) @@ -1889,35 +2029,40 @@ async def test_async_reflect_fixed_value( DEFAULT_DECIMALS = getcontext().prec +NO_MATCHING_ARGUMENTS = "The provided arguments are not valid.\n" +MULTIPLE_MATCHING_ELEMENTS = ( + r"Found multiple elements named `.*` that accept 1 argument\(s\).\n" +) + @pytest.mark.asyncio @pytest.mark.parametrize( "function, value, error", ( # out of range - ("reflect_short_u", Decimal("25.6"), "no matching argument types"), - ("reflect_short_u", Decimal("-.1"), "no matching argument types"), + ("reflect_short_u", Decimal("25.6"), NO_MATCHING_ARGUMENTS), + ("reflect_short_u", Decimal("-.1"), NO_MATCHING_ARGUMENTS), # too many digits for *x1, too large for 256x80 - ("reflect", Decimal("0.01"), "no matching argument types"), + ("reflect(ufixed256x80)", Decimal("0.01"), MULTIPLE_MATCHING_ELEMENTS), # too many digits - ("reflect_short_u", Decimal("0.01"), "no matching argument types"), + ("reflect_short_u", Decimal("0.01"), NO_MATCHING_ARGUMENTS), ( "reflect_short_u", Decimal(f"1e-{DEFAULT_DECIMALS + 1}"), - "no matching argument types", + NO_MATCHING_ARGUMENTS, ), ( "reflect_short_u", Decimal("25.4" + "9" * DEFAULT_DECIMALS), - "no matching argument types", + NO_MATCHING_ARGUMENTS, ), - ("reflect", Decimal(1) / 10**81, "no matching argument types"), + ("reflect(ufixed256x80)", Decimal(1) / 10**81, MULTIPLE_MATCHING_ELEMENTS), # floats not accepted, for floating point error concerns - ("reflect_short_u", 0.1, "no matching argument types"), + ("reflect_short_u", 0.1, NO_MATCHING_ARGUMENTS), # ambiguous - ("reflect", Decimal("12.7"), "Ambiguous argument encoding"), - ("reflect", Decimal(0), "Ambiguous argument encoding"), - ("reflect", 0, "Ambiguous argument encoding"), + ("reflect(ufixed256x80)", Decimal("12.7"), MULTIPLE_MATCHING_ELEMENTS), + ("reflect(ufixed256x80)", Decimal(0), MULTIPLE_MATCHING_ELEMENTS), + ("reflect(ufixed256x80)", 0, MULTIPLE_MATCHING_ELEMENTS), ), ) async def test_async_invalid_fixed_value_reflections( diff --git a/tests/core/contracts/test_contract_class_construction.py b/tests/core/contracts/test_contract_class_construction.py index 9c74dc8c87..2c9c7d3b92 100644 --- a/tests/core/contracts/test_contract_class_construction.py +++ b/tests/core/contracts/test_contract_class_construction.py @@ -9,7 +9,12 @@ Contract, ) from web3.exceptions import ( + ABIEventNotFound, ABIFallbackNotFound, + ABIFunctionNotFound, + NoABIEventsFound, + NoABIFound, + NoABIFunctionsFound, Web3AttributeError, ) @@ -43,6 +48,51 @@ def test_abi_as_json_string(w3, math_contract_abi, some_address): assert math.abi == math_contract_abi +def test_contract_init_with_abi_function_name( + w3, + function_name_tester_contract_abi, + function_name_tester_contract, +): + # test `abi` function name does not throw when creating the contract factory + contract_factory = w3.eth.contract(abi=function_name_tester_contract_abi) + + # re-instantiate the contract + contract = contract_factory(function_name_tester_contract.address) + + # Contract `abi`` function should not override `web3` attribute + assert contract.abi == function_name_tester_contract_abi + + with pytest.raises(TypeError): + contract.functions.abi[0] + + # assert the `abi` function returns true when called + result = contract.functions.abi().call() + assert result is True + + +@pytest.mark.asyncio +async def test_async_contract_init_with_abi_function_name( + async_w3, + function_name_tester_contract_abi, + async_function_name_tester_contract, +): + # test `abi` function name does not throw when creating the contract factory + contract_factory = async_w3.eth.contract(abi=function_name_tester_contract_abi) + + # re-instantiate the contract + contract = contract_factory(async_function_name_tester_contract.address) + + # Contract `abi` function should not override `web3` attribute + assert contract.abi == function_name_tester_contract_abi + + with pytest.raises(TypeError): + contract.functions.abi[0] + + # assert the `abi` function returns true when called + result = await contract.functions.abi().call() + assert result is True + + def test_contract_init_with_w3_function_name( w3, function_name_tester_contract_abi, @@ -54,6 +104,12 @@ def test_contract_init_with_w3_function_name( # re-instantiate the contract contract = contract_factory(function_name_tester_contract.address) + # Contract w3 function should not override web3 instance + with pytest.raises(AttributeError): + contract.functions.w3.eth.get_block("latest") + + assert contract.w3.eth.get_block("latest") is not None + # assert the `w3` function returns true when called result = contract.functions.w3().call() assert result is True @@ -71,6 +127,12 @@ async def test_async_contract_init_with_w3_function_name( # re-instantiate the contract contract = contract_factory(async_function_name_tester_contract.address) + # Contract w3 function should not override web3 instance + with pytest.raises(AttributeError): + contract.functions.w3.eth.get_block("latest") + + assert contract.w3.eth.get_block("latest") is not None + # assert the `w3` function returns true when called result = await contract.functions.w3().call() assert result is True @@ -86,3 +148,53 @@ def test_error_to_call_non_existent_fallback( ) with pytest.raises(ABIFallbackNotFound): math_contract.fallback.estimate_gas() + + +@pytest.mark.parametrize( + "abi,namespace,expected_exception", + ( + (None, "functions", NoABIFound), + (None, "events", NoABIFound), + ([{"type": "event", "name": "AnEvent"}], "functions", NoABIFunctionsFound), + ([{"type": "function", "name": "aFunction"}], "events", NoABIEventsFound), + ([{"type": "function", "name": "aFunction"}], "functions", ABIFunctionNotFound), + ([{"type": "event", "name": "AnEvent"}], "events", ABIEventNotFound), + ), +) +def test_appropriate_exceptions_based_on_namespaces( + w3, abi, namespace, expected_exception +): + if abi is None: + contract = w3.eth.contract() + else: + contract = w3.eth.contract(abi=abi) + + namespace_instance = getattr(contract, namespace) + + with pytest.raises(expected_exception): + namespace_instance.doesNotExist() + + +@pytest.mark.parametrize( + "abi,namespace,expected_exception", + ( + (None, "functions", NoABIFound), + (None, "events", NoABIFound), + ([{"type": "event", "name": "AnEvent"}], "functions", NoABIFunctionsFound), + ([{"type": "function", "name": "aFunction"}], "events", NoABIEventsFound), + ([{"type": "function", "name": "aFunction"}], "functions", ABIFunctionNotFound), + ([{"type": "event", "name": "AnEvent"}], "events", ABIEventNotFound), + ), +) +def test_async_appropriate_exceptions_based_on_namespaces( + async_w3, abi, namespace, expected_exception +): + if abi is None: + contract = async_w3.eth.contract() + else: + contract = async_w3.eth.contract(abi=abi) + + namespace_instance = getattr(contract, namespace) + + with pytest.raises(expected_exception): + namespace_instance.doesNotExist() diff --git a/tests/core/contracts/test_contract_method_to_argument_matching.py b/tests/core/contracts/test_contract_method_to_argument_matching.py index f4674c7e1f..252049e785 100644 --- a/tests/core/contracts/test_contract_method_to_argument_matching.py +++ b/tests/core/contracts/test_contract_method_to_argument_matching.py @@ -1,5 +1,6 @@ import json import pytest +import re from eth_utils.abi import ( get_abi_input_types, @@ -168,14 +169,61 @@ def test_finds_function_with_matching_args_non_strict( def test_finds_function_with_matching_args_strict_type_checking_by_default(w3): contract = w3.eth.contract(abi=MULTIPLE_FUNCTIONS) - with pytest.raises(MismatchedABI): + with pytest.raises( + MismatchedABI, + match=re.escape( + "\nABI Not Found!\n" + "Found multiple elements named `a` that accept 1 argument(s).\n" + "Provided argument types: (str)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `a`, but encountered the " + "following problems:\n" + "Signature: a((int256,bool)[]), type: function\n" + "Arguments do not match types in `a((int256,bool)[])`.\n" + 'Error: Expected non-string sequence for "tuple[]" component type: got \n' + "Signature: a(bytes32), type: function\n" + "Argument 1 value `` is not compatible with type `bytes32`.\n" + "Signature: a(uint256), type: function\n" + "Argument 1 value `` is not compatible with type `uint256`.\n" + "Signature: a(uint8), type: function\n" + "Argument 1 value `` is not compatible with type `uint8`.\n" + "Signature: a(int8), type: function\n" + "Argument 1 value `` is not compatible with type `int8`.\n" + "Signature: a(), type: function\n" + "Expected 0 argument(s) but received 1 argument(s).\n" + ), + ): contract._find_matching_fn_abi("a", *[""]) def test_error_when_duplicate_match(w3): Contract = w3.eth.contract(abi=MULTIPLE_FUNCTIONS) - with pytest.raises(MismatchedABI): + with pytest.raises( + MismatchedABI, + match=re.escape( + "\nABI Not Found!\n" + "Found multiple elements named `a` that accept 1 argument(s).\n" + "Provided argument types: (int)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `a`, but encountered the " + "following problems:\n" + "Signature: a((int256,bool)[]), type: function\n" + "Arguments do not match types in `a((int256,bool)[])`.\n" + 'Error: Expected non-string sequence for "tuple[]" component type: ' + "got 100\n" + "Signature: a(bytes32), type: function\n" + "Argument 1 value `100` is not compatible with type `bytes32`.\n" + "Signature: a(uint256), type: function\n" + "Argument 1 value `100` is valid.\n" + "Signature: a(uint8), type: function\n" + "Argument 1 value `100` is valid.\n" + "Signature: a(int8), type: function\n" + "Argument 1 value `100` is valid.\n" + "Signature: a(), type: function\n" + "Expected 0 argument(s) but received 1 argument(s).\n" + ), + ): Contract._find_matching_fn_abi("a", *[100]) @@ -183,7 +231,13 @@ def test_error_when_duplicate_match(w3): def test_strict_errors_if_type_is_wrong(w3, arguments): Contract = w3.eth.contract(abi=MULTIPLE_FUNCTIONS) - with pytest.raises(MismatchedABI): + with pytest.raises( + MismatchedABI, + match=re.escape( + "\nABI Not Found!\n" + "Found multiple elements named `a` that accept 1 argument(s).\n" + ), + ): Contract._find_matching_fn_abi("a", *arguments) diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py index 3bd2a046e3..6e8d39024c 100644 --- a/tests/core/utilities/test_abi.py +++ b/tests/core/utilities/test_abi.py @@ -1,4 +1,8 @@ +from decimal import ( + Decimal, +) import pytest +import re from typing import ( Any, Callable, @@ -117,7 +121,13 @@ } SET_VALUE_ABI: ABIFunction = { - "inputs": [{"name": "_arg0", "type": "uint256"}], + "inputs": [{"name": "_arg0", "type": "fixed8x1"}], + "name": "setValue", + "stateMutability": "nonpayable", + "type": "function", +} +SET_VALUE_ABI_UFIXED: ABIFunction = { + "inputs": [{"name": "_arg0", "type": "ufixed256x80"}], "name": "setValue", "stateMutability": "nonpayable", "type": "function", @@ -139,13 +149,32 @@ "type": "function", } +AMBIGUOUS_EVENT_ABI: ABIEvent = { + "anonymous": False, + "inputs": [{"name": "arg0", "type": "uint256"}], + "name": "LogSingleArg", + "type": "event", +} +AMBIGUOUS_EVENT_ABI_NO_INPUTS: ABIEvent = { + "anonymous": False, + "inputs": [], + "name": "LogSingleArg", + "type": "event", +} + CONTRACT_ABI: ABI = [ LOG_TWO_EVENTS_ABI, SET_VALUE_ABI, + SET_VALUE_ABI_UFIXED, SET_VALUE_WITH_TUPLE_ABI, FUNCTION_ABI_NO_INPUTS, ] +CONTRACT_ABI_AMBIGUOUS_EVENT: ABI = [ + AMBIGUOUS_EVENT_ABI, + AMBIGUOUS_EVENT_ABI_NO_INPUTS, +] + ABI_CONSTRUCTOR = ABIConstructor({"type": "constructor"}) ABI_FALLBACK = ABIFallback({"type": "fallback"}) @@ -341,7 +370,7 @@ def test_recursive_dict_to_namedtuple( "abi_element_identifier,args,kwargs,expected_selector,expected_arguments", [ ("logTwoEvents", [100], {}, "0x5818fad7", (100,)), - ("setValue", [99], {}, "0x55241077", (99,)), + ("setValue(fixed8x1)", [Decimal("1")], {}, "0xcf20bde8", (Decimal("1"),)), ( "setValue", [1], @@ -385,7 +414,10 @@ def test_get_abi_element_info_without_args_and_kwargs( def test_get_abi_element_info_raises_mismatched_abi(contract_abi: ABI) -> None: - with pytest.raises(MismatchedABI, match="Could not identify the intended function"): + with pytest.raises( + MismatchedABI, + match=r"\nABI Not Found!\nNo element named `foo` with 1 argument\(s\).\n", # noqa: E501 + ): args: Sequence[Any] = [1] get_abi_element_info(contract_abi, "foo", *args, **{}) @@ -395,10 +427,10 @@ def test_get_abi_element_info_raises_mismatched_abi(contract_abi: ABI) -> None: ( ( CONTRACT_ABI, - "setValue", + "setValue(ufixed256x80)", [0], {}, - SET_VALUE_ABI, + SET_VALUE_ABI_UFIXED, ), ( CONTRACT_ABI, @@ -419,6 +451,13 @@ def test_get_abi_element_info_raises_mismatched_abi(contract_abi: ABI) -> None: {}, LOG_TWO_EVENTS_ABI, ), + ( + CONTRACT_ABI, + "logTwoEvents", + [], # function name is unique so args are optional + {}, + LOG_TWO_EVENTS_ABI, + ), ( [FUNCTION_ABI_NO_INPUTS], "myFunction", @@ -479,6 +518,13 @@ def test_get_abi_element_info_raises_mismatched_abi(contract_abi: ABI) -> None: {}, ABI_CONSTRUCTOR, ), + ( + CONTRACT_ABI_AMBIGUOUS_EVENT, + "LogSingleArg()", + [], + {}, + AMBIGUOUS_EVENT_ABI_NO_INPUTS, + ), ), ) def test_get_abi_element( @@ -512,11 +558,134 @@ def test_get_abi_element( ), ( CONTRACT_ABI, - "logTwoEvents", + "setValue", + [], # function name is ambiguous and cannot be determined without args + {}, + MismatchedABI, + "\nABI Not Found!\n" + "Found multiple elements named `setValue` that accept 0 argument(s).\n" + "Provided argument types: ()\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setValue`, but encountered " + "the following problems:\n" + "Signature: setValue(fixed8x1), type: function\n" + "Expected 1 argument(s) but received 0 argument(s).\n" + "Signature: setValue(ufixed256x80), type: function\n" + "Expected 1 argument(s) but received 0 argument(s).\n" + "Signature: setValue(uint256,(uint256,uint256)), type: function\n" + "Expected 2 argument(s) but received 0 argument(s).\n", + ), + ( + CONTRACT_ABI, + "setValue", + [ + Decimal("0") + ], # function name is ambiguous and cannot be determined without args + {}, + MismatchedABI, + "\nABI Not Found!\n" + "Found multiple elements named `setValue` that accept 1 argument(s).\n" + "Provided argument types: (Decimal)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setValue`, but encountered " + "the following problems:\n" + "Signature: setValue(fixed8x1), type: function\n" + "Argument 1 value `0` is valid.\n" + "Signature: setValue(ufixed256x80), type: function\n" + "Argument 1 value `0` is valid.\n" + "Signature: setValue(uint256,(uint256,uint256)), type: function\n" + "Expected 2 argument(s) but received 1 argument(s).\n", + ), + ( + CONTRACT_ABI, + "setValue", + [1, (1, "foo")], + {}, + MismatchedABI, + "\nABI Not Found!\n" + "Found 1 element(s) named `setValue` that accept 2 argument(s).\n" + "The provided arguments are not valid.\n" + "Provided argument types: (int,int,str)\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `setValue`, but encountered " + "the following problems:\n" + "Signature: setValue(uint256,(uint256,uint256)), type: function\n" + "Argument 1 value `1` is valid.\n" + "Argument 2 value `(1, 'foo')` is not compatible with type `(uint256,uint256)`.\n" # noqa: E501 + "Signature: setValue(fixed8x1), type: function\n" + "Expected 1 argument(s) but received 2 argument(s).\n" + "Signature: setValue(ufixed256x80), type: function\n" + "Expected 1 argument(s) but received 2 argument(s).\n", + ), + ( + CONTRACT_ABI_AMBIGUOUS_EVENT, + "LogSingleArg", + [], + {}, + MismatchedABI, + "\nABI Not Found!\n" + "Found multiple elements named `LogSingleArg` that accept 0 argument(s).\n" + "Provided argument types: ()\n" + "Provided keyword argument types: {}\n\n" + "Tried to find a matching ABI element named `LogSingleArg`, but " + "encountered the following problems:\n" + "The provided identifier matches multiple elements.\n" + "If you meant to call `LogSingleArg()`, please specify the full " + "signature.\n" + " - signature: LogSingleArg(), type: event\n" + "Signature: LogSingleArg(uint256), type: event\n" + "Expected 1 argument(s) but received 0 argument(s).\n", + ), + ( + CONTRACT_ABI_AMBIGUOUS_EVENT, + "noFunc", + [], + {}, + MismatchedABI, + "No element named `noFunc` with 0 argument(s).\n", + ), + ( + CONTRACT_ABI_AMBIGUOUS_EVENT, + "noFunc(uint256)", + [], + {}, + MismatchedABI, + "No element named `noFunc` with 0 argument(s).\n", + ), + ( + [ + {"type": "function", "name": "Nonexistent"}, + {"type": "event", "name": "Nonexistent"}, + ], + "Nonexistent", [], {}, MismatchedABI, - "Function invocation failed due to improper number of arguments.", + "", + ), + ( + [{}], + "nonexistent", + [], + {}, + Web3ValueError, + "'abi' must contain a list of elements each with a type", + ), + ( + {}, + "nonexistent", + [], + {}, + Web3ValueError, + "'abi' is not a list", + ), + ( + "ABI", + "nonexistent", + [], + {}, + Web3ValueError, + "'abi' is not a list", ), ), ) @@ -528,8 +697,8 @@ def test_get_abi_element_raises_with_invalid_parameters( expected_error: Type[Exception], expected_message: str, ) -> None: - with pytest.raises(expected_error, match=expected_message): - get_abi_element(abi, abi_element_identifier, *args, **kwargs) # type: ignore + with pytest.raises(expected_error, match=re.escape(expected_message)): + get_abi_element(abi, abi_element_identifier, *args, **kwargs) def test_get_abi_element_codec_override(contract_abi: ABI) -> None: @@ -647,7 +816,14 @@ def test_get_event_abi(event_name: str, input_args: Sequence[ABIComponent]) -> N } input_names = [arg["name"] for arg in input_args] - assert get_event_abi(contract_abi, event_name, input_names) == expected_event_abi + + with pytest.warns( + DeprecationWarning, + match="get_event_abi is deprecated in favor of get_abi_element", + ): + assert ( + get_event_abi(contract_abi, event_name, input_names) == expected_event_abi + ) @pytest.mark.parametrize( diff --git a/tests/core/utilities/test_event_interface.py b/tests/core/utilities/test_event_interface.py index 7a87fdb1ea..225644848b 100644 --- a/tests/core/utilities/test_event_interface.py +++ b/tests/core/utilities/test_event_interface.py @@ -3,6 +3,7 @@ from web3.exceptions import ( MismatchedABI, NoABIEventsFound, + NoABIFound, ) EVENT_1_ABI = { @@ -18,7 +19,7 @@ def test_access_event_with_no_abi(w3): contract = w3.eth.contract() - with pytest.raises(NoABIEventsFound): + with pytest.raises(NoABIFound): contract.events.thisEventDoesNotExist() diff --git a/tests/core/utilities/test_validation.py b/tests/core/utilities/test_validation.py index 224bb05aab..6ef65a2e3e 100644 --- a/tests/core/utilities/test_validation.py +++ b/tests/core/utilities/test_validation.py @@ -86,6 +86,7 @@ (MALFORMED_ABI_2, validate_abi, Web3ValueError), (MALFORMED_SELECTOR_COLLISION_ABI, validate_abi, Web3ValueError), (MALFORMED_SIGNATURE_COLLISION_ABI, validate_abi, Web3ValueError), + ([{}], validate_abi, Web3ValueError), (ADDRESS, validate_address, None), (BYTES_ADDRESS, validate_address, None), (PADDED_ADDRESS, validate_address, InvalidAddress), diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index f4d669f958..16b442ef4c 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -63,6 +63,7 @@ decode_hex, filter_abi_by_type, get_abi_input_names, + get_abi_input_types, is_bytes, is_list_like, is_string, @@ -76,6 +77,10 @@ pipe, ) +from web3._utils.abi_element_identifiers import ( + FallbackFn, + ReceiveFn, +) from web3._utils.decorators import ( reject_recursive_repeats, ) @@ -92,6 +97,7 @@ Web3ValueError, ) from web3.types import ( + ABIElementIdentifier, TReturn, ) @@ -117,9 +123,13 @@ def exclude_indexed_event_inputs(event_abi: ABIEvent) -> Sequence[ABIComponentIn return [arg for arg in event_abi["inputs"] if arg["indexed"] is False] +def filter_by_types(types: Collection[str], contract_abi: ABI) -> Sequence[ABIElement]: + return [abi_element for abi_element in contract_abi if abi_element["type"] in types] + + def filter_by_argument_name( argument_names: Collection[str], contract_abi: ABI -) -> List[ABIElement]: +) -> Sequence[ABIElement]: """ Return a list of each ``ABIElement`` which contain arguments matching provided names. @@ -139,6 +149,56 @@ def filter_by_argument_name( return abis_with_matching_args +def filter_by_argument_type( + argument_types: Collection[str], contract_abi: ABI +) -> List[ABIElement]: + """ + Return a list of each ``ABIElement`` which contain arguments matching provided + types. + """ + abis_with_matching_args = [] + for abi_element in contract_abi: + try: + abi_arg_types = get_abi_input_types(abi_element) + + if set(argument_types).intersection(abi_arg_types) == set(argument_types): + abis_with_matching_args.append(abi_element) + except ValueError: + # fallback or receive functions do not have arguments + # proceed to next ABIElement + continue + + return abis_with_matching_args + + +def get_name_from_abi_element_identifier( + abi_element_identifier: ABIElementIdentifier, +) -> str: + if abi_element_identifier in ["fallback", FallbackFn]: + return "fallback" + elif abi_element_identifier in ["receive", ReceiveFn]: + return "receive" + elif abi_element_identifier == "constructor": + return "constructor" + elif is_text(abi_element_identifier): + return str(abi_element_identifier).split("(")[0] + else: + raise Web3TypeError("Unsupported function identifier") + + +def get_abi_element_signature( + abi_element_identifier: ABIElementIdentifier, + abi_element_argument_types: Optional[Sequence[str]] = None, +) -> str: + element_name = get_name_from_abi_element_identifier(abi_element_identifier) + argument_types = ",".join(abi_element_argument_types or []) + + if element_name in ["fallback", "receive", "constructor"]: + return element_name + + return f"{element_name}({argument_types})" + + class AddressEncoder(encoding.AddressEncoder): @classmethod def validate_value(cls, value: Any) -> None: diff --git a/web3/_utils/contract_sources/EventContracts.sol b/web3/_utils/contract_sources/EventContracts.sol index 2171c24982..7e757b6d20 100644 --- a/web3/_utils/contract_sources/EventContracts.sol +++ b/web3/_utils/contract_sources/EventContracts.sol @@ -19,3 +19,13 @@ contract IndexedEventContract { emit LogSingleArg(_arg0); } } + +contract AmbiguousEventNameContract { + event LogSingleArg(uint256 arg0); + event LogSingleArg(bytes32 arg0); + + function logTwoEvents(uint256 _arg0) external { + emit LogSingleArg(_arg0); + emit LogSingleArg(bytes32(abi.encode(_arg0))); + } +} diff --git a/web3/_utils/contract_sources/contract_data/event_contracts.py b/web3/_utils/contract_sources/contract_data/event_contracts.py index 98a534f722..0a168640e0 100644 --- a/web3/_utils/contract_sources/contract_data/event_contracts.py +++ b/web3/_utils/contract_sources/contract_data/event_contracts.py @@ -1,46 +1,12 @@ """ Generated by `compile_contracts.py` script. -Compiled with Solidity v0.8.28. +Compiled with Solidity v0.8.27. """ # source: web3/_utils/contract_sources/EventContracts.sol:EventContract -EVENT_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b5061017a8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100f1565b610049565b005b7ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1581604051610078919061012b565b60405180910390a17f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100af919061012b565b60405180910390a150565b5f5ffd5b5f819050919050565b6100d0816100be565b81146100da575f5ffd5b50565b5f813590506100eb816100c7565b92915050565b5f60208284031215610106576101056100ba565b5b5f610113848285016100dd565b91505092915050565b610125816100be565b82525050565b5f60208201905061013e5f83018461011c565b9291505056fea2646970667358221220ffb25fdd3a92ed8f87f68da4885d8c0f5e6fd2edeafb3d1cab0da0f2683b846b64736f6c634300081c0033" # noqa: E501 -EVENT_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100f1565b610049565b005b7ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1581604051610078919061012b565b60405180910390a17f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100af919061012b565b60405180910390a150565b5f5ffd5b5f819050919050565b6100d0816100be565b81146100da575f5ffd5b50565b5f813590506100eb816100c7565b92915050565b5f60208284031215610106576101056100ba565b5b5f610113848285016100dd565b91505092915050565b610125816100be565b82525050565b5f60208201905061013e5f83018461011c565b9291505056fea2646970667358221220ffb25fdd3a92ed8f87f68da4885d8c0f5e6fd2edeafb3d1cab0da0f2683b846b64736f6c634300081c0033" # noqa: E501 -EVENT_CONTRACT_ABI = [ - { - "anonymous": False, - "inputs": [ - { - "indexed": False, - "internalType": "uint256", - "name": "arg0", - "type": "uint256", - } - ], - "name": "LogSingleArg", - "type": "event", - }, - { - "anonymous": False, - "inputs": [ - { - "indexed": False, - "internalType": "uint256", - "name": "arg0", - "type": "uint256", - } - ], - "name": "LogSingleWithIndex", - "type": "event", - }, - { - "inputs": [{"internalType": "uint256", "name": "_arg0", "type": "uint256"}], - "name": "logTwoEvents", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function", - }, -] +EVENT_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b5061017a8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100f1565b610049565b005b7ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1581604051610078919061012b565b60405180910390a17f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100af919061012b565b60405180910390a150565b5f5ffd5b5f819050919050565b6100d0816100be565b81146100da575f5ffd5b50565b5f813590506100eb816100c7565b92915050565b5f60208284031215610106576101056100ba565b5b5f610113848285016100dd565b91505092915050565b610125816100be565b82525050565b5f60208201905061013e5f83018461011c565b9291505056fea2646970667358221220b0bcf8d91a48b57e8c3a13a2358baf5a199c99a22aebe7940a1dc0397f284b9064736f6c634300081b0033" # noqa: E501 +EVENT_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100f1565b610049565b005b7ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1581604051610078919061012b565b60405180910390a17f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100af919061012b565b60405180910390a150565b5f5ffd5b5f819050919050565b6100d0816100be565b81146100da575f5ffd5b50565b5f813590506100eb816100c7565b92915050565b5f60208284031215610106576101056100ba565b5b5f610113848285016100dd565b91505092915050565b610125816100be565b82525050565b5f60208201905061013e5f83018461011c565b9291505056fea2646970667358221220b0bcf8d91a48b57e8c3a13a2358baf5a199c99a22aebe7940a1dc0397f284b9064736f6c634300081b0033" # noqa: E501 +EVENT_CONTRACT_ABI = [{'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleArg', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleWithIndex', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '_arg0', 'type': 'uint256'}], 'name': 'logTwoEvents', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}] EVENT_CONTRACT_DATA = { "bytecode": EVENT_CONTRACT_BYTECODE, "bytecode_runtime": EVENT_CONTRACT_RUNTIME, @@ -49,45 +15,24 @@ # source: web3/_utils/contract_sources/EventContracts.sol:IndexedEventContract -INDEXED_EVENT_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b506101708061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100e7565b610049565b005b807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560405160405180910390a27f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100a59190610121565b60405180910390a150565b5f5ffd5b5f819050919050565b6100c6816100b4565b81146100d0575f5ffd5b50565b5f813590506100e1816100bd565b92915050565b5f602082840312156100fc576100fb6100b0565b5b5f610109848285016100d3565b91505092915050565b61011b816100b4565b82525050565b5f6020820190506101345f830184610112565b9291505056fea2646970667358221220c54729ba3e926e84d8497888e2dc66bd33efa5e4ebcd95e323dc53eb183500cc64736f6c634300081c0033" # noqa: E501 -INDEXED_EVENT_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100e7565b610049565b005b807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560405160405180910390a27f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100a59190610121565b60405180910390a150565b5f5ffd5b5f819050919050565b6100c6816100b4565b81146100d0575f5ffd5b50565b5f813590506100e1816100bd565b92915050565b5f602082840312156100fc576100fb6100b0565b5b5f610109848285016100d3565b91505092915050565b61011b816100b4565b82525050565b5f6020820190506101345f830184610112565b9291505056fea2646970667358221220c54729ba3e926e84d8497888e2dc66bd33efa5e4ebcd95e323dc53eb183500cc64736f6c634300081c0033" # noqa: E501 -INDEXED_EVENT_CONTRACT_ABI = [ - { - "anonymous": False, - "inputs": [ - { - "indexed": False, - "internalType": "uint256", - "name": "arg0", - "type": "uint256", - } - ], - "name": "LogSingleArg", - "type": "event", - }, - { - "anonymous": False, - "inputs": [ - { - "indexed": True, - "internalType": "uint256", - "name": "arg0", - "type": "uint256", - } - ], - "name": "LogSingleWithIndex", - "type": "event", - }, - { - "inputs": [{"internalType": "uint256", "name": "_arg0", "type": "uint256"}], - "name": "logTwoEvents", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function", - }, -] +INDEXED_EVENT_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b506101708061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100e7565b610049565b005b807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560405160405180910390a27f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100a59190610121565b60405180910390a150565b5f5ffd5b5f819050919050565b6100c6816100b4565b81146100d0575f5ffd5b50565b5f813590506100e1816100bd565b92915050565b5f602082840312156100fc576100fb6100b0565b5b5f610109848285016100d3565b91505092915050565b61011b816100b4565b82525050565b5f6020820190506101345f830184610112565b9291505056fea2646970667358221220d6260750ec274a719dbff1334e0c10993ba5e8b99bd2807ffd773256ed41a2e164736f6c634300081b0033" # noqa: E501 +INDEXED_EVENT_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100e7565b610049565b005b807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560405160405180910390a27f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100a59190610121565b60405180910390a150565b5f5ffd5b5f819050919050565b6100c6816100b4565b81146100d0575f5ffd5b50565b5f813590506100e1816100bd565b92915050565b5f602082840312156100fc576100fb6100b0565b5b5f610109848285016100d3565b91505092915050565b61011b816100b4565b82525050565b5f6020820190506101345f830184610112565b9291505056fea2646970667358221220d6260750ec274a719dbff1334e0c10993ba5e8b99bd2807ffd773256ed41a2e164736f6c634300081b0033" # noqa: E501 +INDEXED_EVENT_CONTRACT_ABI = [{'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleArg', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': True, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleWithIndex', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '_arg0', 'type': 'uint256'}], 'name': 'logTwoEvents', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}] INDEXED_EVENT_CONTRACT_DATA = { "bytecode": INDEXED_EVENT_CONTRACT_BYTECODE, "bytecode_runtime": INDEXED_EVENT_CONTRACT_RUNTIME, "abi": INDEXED_EVENT_CONTRACT_ABI, } + + +# source: web3/_utils/contract_sources/EventContracts.sol:AmbiguousEventNameContract +AMBIGUOUS_EVENT_NAME_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b506102728061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b61004760048036038101906100429190610119565b610049565b005b7f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100789190610153565b60405180910390a17fe466ad4edc182e32048f6e723b179ae20d1030f298fcfa1e9ad4a759b5a63112816040516020016100b29190610153565b6040516020818303038152906040526100ca906101ae565b6040516100d79190610223565b60405180910390a150565b5f5ffd5b5f819050919050565b6100f8816100e6565b8114610102575f5ffd5b50565b5f81359050610113816100ef565b92915050565b5f6020828403121561012e5761012d6100e2565b5b5f61013b84828501610105565b91505092915050565b61014d816100e6565b82525050565b5f6020820190506101665f830184610144565b92915050565b5f81519050919050565b5f819050602082019050919050565b5f819050919050565b5f6101998251610185565b80915050919050565b5f82821b905092915050565b5f6101b88261016c565b826101c284610176565b90506101cd8161018e565b9250602082101561020d576102087fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff836020036008026101a2565b831692505b5050919050565b61021d81610185565b82525050565b5f6020820190506102365f830184610214565b9291505056fea2646970667358221220e921a9ff848699a59d4bfe4f7fe3c6ea2c735d2d7bee0857548a605a86827b3664736f6c634300081b0033" # noqa: E501 +AMBIGUOUS_EVENT_NAME_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b61004760048036038101906100429190610119565b610049565b005b7f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100789190610153565b60405180910390a17fe466ad4edc182e32048f6e723b179ae20d1030f298fcfa1e9ad4a759b5a63112816040516020016100b29190610153565b6040516020818303038152906040526100ca906101ae565b6040516100d79190610223565b60405180910390a150565b5f5ffd5b5f819050919050565b6100f8816100e6565b8114610102575f5ffd5b50565b5f81359050610113816100ef565b92915050565b5f6020828403121561012e5761012d6100e2565b5b5f61013b84828501610105565b91505092915050565b61014d816100e6565b82525050565b5f6020820190506101665f830184610144565b92915050565b5f81519050919050565b5f819050602082019050919050565b5f819050919050565b5f6101998251610185565b80915050919050565b5f82821b905092915050565b5f6101b88261016c565b826101c284610176565b90506101cd8161018e565b9250602082101561020d576102087fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff836020036008026101a2565b831692505b5050919050565b61021d81610185565b82525050565b5f6020820190506102365f830184610214565b9291505056fea2646970667358221220e921a9ff848699a59d4bfe4f7fe3c6ea2c735d2d7bee0857548a605a86827b3664736f6c634300081b0033" # noqa: E501 +AMBIGUOUS_EVENT_NAME_CONTRACT_ABI = [{'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleArg', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'bytes32', 'name': 'arg0', 'type': 'bytes32'}], 'name': 'LogSingleArg', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '_arg0', 'type': 'uint256'}], 'name': 'logTwoEvents', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}] +AMBIGUOUS_EVENT_NAME_CONTRACT_DATA = { + "bytecode": AMBIGUOUS_EVENT_NAME_CONTRACT_BYTECODE, + "bytecode_runtime": AMBIGUOUS_EVENT_NAME_CONTRACT_RUNTIME, + "abi": AMBIGUOUS_EVENT_NAME_CONTRACT_ABI, +} + + diff --git a/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py b/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py index e4b6f04b6d..51e96c88f4 100644 --- a/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +++ b/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py @@ -1,29 +1,16 @@ """ Generated by `compile_contracts.py` script. -Compiled with Solidity v0.8.28. +Compiled with Solidity v0.8.27. """ # source: web3/_utils/contract_sources/FunctionNameTesterContract.sol:FunctionNameTesterContract # noqa: E501 -FUNCTION_NAME_TESTER_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b5060dc80601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c8063a044c987146034578063c5d7802e14604e575b5f5ffd5b603a6068565b60405160459190608f565b60405180910390f35b60546070565b604051605f9190608f565b60405180910390f35b5f6001905090565b5f5f905090565b5f8115159050919050565b6089816077565b82525050565b5f60208201905060a05f8301846082565b9291505056fea2646970667358221220b639c6d170602b8e1800b59672ba79eccf6c325c201b1889e36ba1bfb02190e264736f6c634300081c0033" # noqa: E501 -FUNCTION_NAME_TESTER_CONTRACT_RUNTIME = "0x6080604052348015600e575f5ffd5b50600436106030575f3560e01c8063a044c987146034578063c5d7802e14604e575b5f5ffd5b603a6068565b60405160459190608f565b60405180910390f35b60546070565b604051605f9190608f565b60405180910390f35b5f6001905090565b5f5f905090565b5f8115159050919050565b6089816077565b82525050565b5f60208201905060a05f8301846082565b9291505056fea2646970667358221220b639c6d170602b8e1800b59672ba79eccf6c325c201b1889e36ba1bfb02190e264736f6c634300081c0033" # noqa: E501 -FUNCTION_NAME_TESTER_CONTRACT_ABI = [ - { - "inputs": [], - "name": "w3", - "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], - "stateMutability": "nonpayable", - "type": "function", - }, - { - "inputs": [], - "name": "z", - "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], - "stateMutability": "nonpayable", - "type": "function", - }, -] +FUNCTION_NAME_TESTER_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b5060dc80601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c8063a044c987146034578063c5d7802e14604e575b5f5ffd5b603a6068565b60405160459190608f565b60405180910390f35b60546070565b604051605f9190608f565b60405180910390f35b5f6001905090565b5f5f905090565b5f8115159050919050565b6089816077565b82525050565b5f60208201905060a05f8301846082565b9291505056fea26469706673582212200a081b5e095d27bf0d18bfdb07b8618bdf1a04e1a4cf660629f12ab2b0d0bdb264736f6c634300081b0033" # noqa: E501 +FUNCTION_NAME_TESTER_CONTRACT_RUNTIME = "0x6080604052348015600e575f5ffd5b50600436106030575f3560e01c8063a044c987146034578063c5d7802e14604e575b5f5ffd5b603a6068565b60405160459190608f565b60405180910390f35b60546070565b604051605f9190608f565b60405180910390f35b5f6001905090565b5f5f905090565b5f8115159050919050565b6089816077565b82525050565b5f60208201905060a05f8301846082565b9291505056fea26469706673582212200a081b5e095d27bf0d18bfdb07b8618bdf1a04e1a4cf660629f12ab2b0d0bdb264736f6c634300081b0033" # noqa: E501 +FUNCTION_NAME_TESTER_CONTRACT_ABI = [{'inputs': [], 'name': 'w3', 'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [], 'name': 'z', 'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}], 'stateMutability': 'nonpayable', 'type': 'function'}] FUNCTION_NAME_TESTER_CONTRACT_DATA = { "bytecode": FUNCTION_NAME_TESTER_CONTRACT_BYTECODE, "bytecode_runtime": FUNCTION_NAME_TESTER_CONTRACT_RUNTIME, "abi": FUNCTION_NAME_TESTER_CONTRACT_ABI, } + + diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index f3d067640a..796c66df63 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -1,3 +1,4 @@ +import copy import functools from typing import ( TYPE_CHECKING, @@ -47,13 +48,11 @@ from web3._utils.abi import ( filter_by_argument_name, + get_abi_element_signature, + get_name_from_abi_element_identifier, map_abi_data, named_tree, ) -from web3._utils.abi_element_identifiers import ( - FallbackFn, - ReceiveFn, -) from web3._utils.blocks import ( is_hex_encoded_block_hash, ) @@ -79,6 +78,8 @@ ABIElementIdentifier, BlockIdentifier, BlockNumber, + TContractEvent, + TContractFn, TxParams, ) from web3.utils.abi import ( @@ -182,6 +183,18 @@ def prepare_transaction( """ fn_args = fn_args or [] fn_kwargs = fn_kwargs or {} + + if not fn_args and not fn_kwargs and "(" not in str(abi_element_identifier): + abi_element_identifier = get_abi_element_signature(abi_element_identifier) + + if abi_element_identifier in [ + "fallback()", + "receive()", + ]: + abi_element_identifier = get_name_from_abi_element_identifier( + abi_element_identifier + ) + if abi_callable is None: abi_callable = cast( ABICallable, @@ -227,11 +240,12 @@ def encode_transaction_data( kwargs: Optional[Any] = None, ) -> HexStr: info_abi: ABIElement - if abi_element_identifier is FallbackFn: + abi_element_name = get_name_from_abi_element_identifier(abi_element_identifier) + if abi_element_name == "fallback": info_abi, info_selector, info_arguments = get_fallback_function_info( contract_abi, cast(ABIFallback, abi_callable) ) - elif abi_element_identifier is ReceiveFn: + elif abi_element_name == "receive": info_abi, info_selector, info_arguments = get_receive_function_info( contract_abi, cast(ABIReceive, abi_callable) ) @@ -378,4 +392,31 @@ async def async_parse_block_identifier_int( if block_num < 0: raise BlockNumberOutOfRange return BlockNumber(block_num) - return BlockNumber(block_num) + + +def copy_contract_function( + contract_function: TContractFn, *args: Any, **kwargs: Any +) -> TContractFn: + """ + Copy a contract function instance. + """ + clone = copy.copy(contract_function) + clone.args = args or tuple() + clone.kwargs = kwargs or dict() + + clone._set_function_info() + return clone + + +def copy_contract_event( + contract_event: TContractEvent, *args: Any, **kwargs: Any +) -> TContractEvent: + """ + Copy a contract function instance. + """ + clone = copy.copy(contract_event) + clone.args = args or tuple() + clone.kwargs = kwargs or dict() + + clone._set_event_info() + return clone diff --git a/web3/_utils/validation.py b/web3/_utils/validation.py index 18093717bd..4241cffa65 100644 --- a/web3/_utils/validation.py +++ b/web3/_utils/validation.py @@ -79,6 +79,9 @@ def validate_abi(abi: ABI) -> None: if not all(is_dict(e) for e in abi): raise Web3ValueError("'abi' is not a list of dictionaries") + if not all("type" in e for e in abi): + raise Web3ValueError("'abi' must contain a list of elements each with a type") + functions = filter_abi_by_type("function", abi) selectors = groupby(compose(encode_hex, function_abi_to_4byte_selector), functions) duplicates = valfilter(lambda funcs: len(funcs) > 1, selectors) diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 0342b8cde0..c0aa352043 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -1,4 +1,3 @@ -import copy from typing import ( TYPE_CHECKING, Any, @@ -22,7 +21,9 @@ combomethod, ) from eth_utils.abi import ( + abi_to_signature, get_abi_input_names, + get_abi_input_types, get_all_function_abis, ) from eth_utils.toolz import ( @@ -34,6 +35,8 @@ from web3._utils.abi import ( fallback_func_abi_exists, + get_abi_element_signature, + get_name_from_abi_element_identifier, receive_func_abi_exists, ) from web3._utils.abi_element_identifiers import ( @@ -48,6 +51,8 @@ ) from web3._utils.contracts import ( async_parse_block_identifier, + copy_contract_event, + copy_contract_function, ) from web3._utils.datatypes import ( PropertyCheckingFactory, @@ -86,7 +91,9 @@ get_function_by_identifier, ) from web3.exceptions import ( + ABIEventNotFound, ABIFunctionNotFound, + NoABIEventsFound, NoABIFound, NoABIFunctionsFound, Web3AttributeError, @@ -101,6 +108,7 @@ TxParams, ) from web3.utils.abi import ( + _get_any_abi_signature_with_name, get_abi_element, ) @@ -113,16 +121,8 @@ class AsyncContractEvent(BaseContractEvent): # mypy types w3: "AsyncWeb3" - def __call__(self) -> "AsyncContractEvent": - clone = copy.copy(self) - - if not self.abi: - self.abi = cast( - ABIEvent, - get_abi_element(self.contract_abi, self.event_name), - ) - - return clone + def __call__(self, *args: Any, **kwargs: Any) -> "AsyncContractEvent": + return copy_contract_event(self, *args, **kwargs) @combomethod async def get_logs( @@ -188,11 +188,9 @@ async def get_logs( same time as ``from_block`` or ``to_block`` :yield: Tuple of :class:`AttributeDict` instances """ - event_abi = self._get_event_abi() - # validate ``argument_filters`` if present if argument_filters is not None: - event_arg_names = get_abi_input_names(event_abi) + event_arg_names = get_abi_input_names(self.abi) if not all(arg in event_arg_names for arg in argument_filters.keys()): raise Web3ValidationError( "When filtering by argument names, all argument names must be " @@ -200,17 +198,17 @@ async def get_logs( ) _filter_params = self._get_event_filter_params( - event_abi, argument_filters, from_block, to_block, block_hash + self.abi, argument_filters, from_block, to_block, block_hash ) # call JSON-RPC API logs = await self.w3.eth.get_logs(_filter_params) # convert raw binary data to Python proxy objects as described by ABI: all_event_logs = tuple( - get_event_data(self.w3.codec, event_abi, entry) for entry in logs + get_event_data(self.w3.codec, self.abi, entry) for entry in logs ) filtered_logs = self._process_get_logs_argument_filters( - event_abi, + self.abi, all_event_logs, argument_filters, ) @@ -231,7 +229,7 @@ async def create_filter( """ Create filter object that tracks logs emitted by this contract event. """ - filter_builder = AsyncEventFilterBuilder(self._get_event_abi(), self.w3.codec) + filter_builder = AsyncEventFilterBuilder(self.abi, self.w3.codec) self._set_up_filter_builder( argument_filters, from_block, @@ -241,9 +239,7 @@ async def create_filter( filter_builder, ) log_filter = await filter_builder.deploy(self.w3) - log_filter.log_entry_formatter = get_event_data( - self.w3.codec, self._get_event_abi() - ) + log_filter.log_entry_formatter = get_event_data(self.w3.codec, self.abi) log_filter.builder = filter_builder return log_filter @@ -251,9 +247,9 @@ async def create_filter( @combomethod def build_filter(self) -> AsyncEventFilterBuilder: builder = AsyncEventFilterBuilder( - self._get_event_abi(), + self.abi, self.w3.codec, - formatter=get_event_data(self.w3.codec, self._get_event_abi()), + formatter=get_event_data(self.w3.codec, self.abi), ) builder.address = self.address return builder @@ -271,24 +267,47 @@ def __init__( ) -> None: super().__init__(abi, w3, AsyncContractEvent, address) + def __iter__(self) -> Iterable["AsyncContractEvent"]: + if not hasattr(self, "_events") or not self._events: + return + + for event in self._events: + yield self[abi_to_signature(event)] + + def __getattr__(self, event_name: str) -> "AsyncContractEvent": + if super().__getattribute__("abi") is None: + raise NoABIFound( + "There is no ABI found for this contract.", + ) + if "_events" not in self.__dict__: + raise NoABIEventsFound( + "The abi for this contract contains no event definitions. ", + "Are you sure you provided the correct contract abi?", + ) + elif get_name_from_abi_element_identifier(event_name) not in [ + get_name_from_abi_element_identifier(event["name"]) + for event in self._events + ]: + raise ABIEventNotFound( + f"The event '{event_name}' was not found in this contract's abi. ", + "Are you sure you provided the correct contract abi?", + ) + else: + event_abi = get_abi_element(self._events, event_name) + argument_types = get_abi_input_types(event_abi) + event_signature = str(get_abi_element_signature(event_name, argument_types)) + return super().__getattribute__(event_signature) + + def __getitem__(self, event_name: str) -> "AsyncContractEvent": + return getattr(self, event_name) + class AsyncContractFunction(BaseContractFunction): # mypy types w3: "AsyncWeb3" def __call__(self, *args: Any, **kwargs: Any) -> "AsyncContractFunction": - clone = copy.copy(self) - if args is None: - clone.args = tuple() - else: - clone.args = args - - if kwargs is None: - clone.kwargs = {} - else: - clone.kwargs = kwargs - clone._set_function_info() - return clone + return copy_contract_function(self, *args, **kwargs) @classmethod def factory(cls, class_name: str, **kwargs: Any) -> Self: @@ -332,11 +351,13 @@ async def call( block_id = await async_parse_block_identifier(self.w3, block_identifier) + abi_element_identifier = abi_to_signature(self.abi) + return await async_call_contract_function( self.w3, self.address, self._return_data_normalizers, - self.abi_element_identifier, + abi_element_identifier, call_transaction, block_id, self.contract_abi, @@ -350,10 +371,11 @@ async def call( async def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: setup_transaction = self._transact(transaction) + abi_element_identifier = abi_to_signature(self.abi) return await async_transact_with_contract_function( self.address, self.w3, - self.abi_element_identifier, + abi_element_identifier, setup_transaction, self.contract_abi, self.abi, @@ -368,10 +390,11 @@ async def estimate_gas( state_override: Optional[StateOverride] = None, ) -> int: setup_transaction = self._estimate_gas(transaction) + abi_element_identifier = abi_to_signature(self.abi) return await async_estimate_gas_for_function( self.address, self.w3, - self.abi_element_identifier, + abi_element_identifier, setup_transaction, self.contract_abi, self.abi, @@ -385,10 +408,11 @@ async def build_transaction( self, transaction: Optional[TxParams] = None ) -> TxParams: built_transaction = self._build_transaction(transaction) + abi_element_identifier = abi_to_signature(self.abi) return await async_build_transaction_for_function( self.address, self.w3, - self.abi_element_identifier, + abi_element_identifier, built_transaction, self.contract_abi, self.abi, @@ -439,23 +463,60 @@ def __init__( ) -> None: super().__init__(abi, w3, AsyncContractFunction, address, decode_tuples) - def __getattr__(self, function_name: str) -> "AsyncContractFunction": - if self.abi is None: + def __iter__(self) -> Iterable["AsyncContractFunction"]: + if not hasattr(self, "_functions") or not self._functions: + return + + for func in self._functions: + yield self[abi_to_signature(func)] + + def __getattribute__(self, function_name: str) -> "AsyncContractFunction": + function_identifier = function_name + + # Function names can override object attributes + if function_name in ["abi", "w3", "address"] and super().__getattribute__( + "_functions" + ): + function_identifier = _get_any_abi_signature_with_name( + function_name, super().__getattribute__("_functions") + ) + + return super().__getattribute__(function_identifier) + + def __getattr__( + self, function_name: str + ) -> Callable[[Any, Any], "AsyncContractFunction"]: + if super().__getattribute__("abi") is None: raise NoABIFound( "There is no ABI found for this contract.", ) - if "_functions" not in self.__dict__: + elif "_functions" not in self.__dict__: raise NoABIFunctionsFound( "The abi for this contract contains no function definitions. ", "Are you sure you provided the correct contract abi?", ) - elif function_name not in self.__dict__["_functions"]: + elif get_name_from_abi_element_identifier(function_name) not in [ + get_name_from_abi_element_identifier(function["name"]) + for function in self._functions + ]: raise ABIFunctionNotFound( - f"The function '{function_name}' was not found in this contract's abi.", - " Are you sure you provided the correct contract abi?", + f"The function '{function_name}' was not found in this contract's " + "abi. Are you sure you provided the correct contract abi?", ) - else: - return super().__getattribute__(function_name) + + function_identifier = function_name + + if "(" not in function_name: + function_identifier = _get_any_abi_signature_with_name( + function_name, self._functions + ) + + return super().__getattribute__( + function_identifier, + ) + + def __getitem__(self, function_name: str) -> "AsyncContractFunction": + return getattr(self, function_name) class AsyncContract(BaseContract): @@ -623,12 +684,12 @@ def __init__( self._functions = get_all_function_abis(self.abi) for func in self._functions: + abi_signature = abi_to_signature(func) fn = AsyncContractFunction.factory( - func["name"], + abi_signature, w3=w3, contract_abi=self.abi, address=self.address, - abi_element_identifier=func["name"], decode_tuples=decode_tuples, ) @@ -640,7 +701,7 @@ def __init__( ccip_read_enabled=ccip_read_enabled, ) - setattr(self, func["name"], caller_method) + setattr(self, abi_signature, caller_method) def __call__( self, diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 54e7e90cda..cb37f14cf7 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -48,7 +48,10 @@ from web3._utils.abi import ( fallback_func_abi_exists, + filter_by_types, find_constructor_abi_element_by_type, + get_abi_element_signature, + get_name_from_abi_element_identifier, is_array_type, receive_func_abi_exists, ) @@ -96,7 +99,6 @@ InvalidEventABI, LogTopicError, MismatchedABI, - NoABIEventsFound, NoABIFound, NoABIFunctionsFound, Web3AttributeError, @@ -121,10 +123,10 @@ TxReceipt, ) from web3.utils.abi import ( + _get_any_abi_signature_with_name, check_if_arguments_can_be_encoded, get_abi_element, get_abi_element_info, - get_event_abi, ) if TYPE_CHECKING: @@ -156,23 +158,31 @@ class BaseContractEvent: w3: Union["Web3", "AsyncWeb3"] = None contract_abi: ABI = None abi: ABIEvent = None + argument_types: Tuple[str] = None + args: Any = None + kwargs: Any = None - def __init__(self, *argument_names: Tuple[str], abi: ABIEvent) -> None: - self.abi = abi - self.name = type(self).__name__ - - if argument_names is None: - # https://github.com/python/mypy/issues/6283 - self.argument_names = tuple() # type: ignore - else: - self.argument_names = argument_names - - def __repr__(self) -> str: - return f"" + def __init__(self, *argument_names: Tuple[str]) -> None: + self.event_name = get_name_from_abi_element_identifier(type(self).__name__) + self.abi_element_identifier = type(self).__name__ + self.abi = self._get_event_abi() - @classmethod + @combomethod def _get_event_abi(cls) -> ABIEvent: - return get_event_abi(cls.contract_abi, event_name=cls.event_name) + if cls.abi: + return cls.abi + + return cast( + ABIEvent, + get_abi_element( + filter_abi_by_type("event", cls.contract_abi), + cls.abi_element_identifier, + abi_codec=cls.w3.codec, + ), + ) + + def _set_event_info(self) -> None: + self.abi = self._get_event_abi() @combomethod def process_receipt( @@ -275,9 +285,7 @@ def _get_event_filter_params( def factory( cls, class_name: str, **kwargs: Any ) -> Union["ContractEvent", "AsyncContractEvent"]: - return PropertyCheckingFactory(class_name, (cls,), kwargs)( - abi=kwargs.get("abi") - ) + return PropertyCheckingFactory(class_name, (cls,), kwargs)() @staticmethod def check_for_forbidden_api_filter_arguments( @@ -367,12 +375,10 @@ def _set_up_filter_builder( _filters = dict(**argument_filters) - event_abi = self._get_event_abi() - - self.check_for_forbidden_api_filter_arguments(event_abi, _filters) + self.check_for_forbidden_api_filter_arguments(self.abi, _filters) _, event_filter_params = construct_event_filter_params( - self._get_event_abi(), + self.abi, self.w3.codec, contract_address=self.address, argument_filters=_filters, @@ -435,48 +441,32 @@ def __init__( contract_event_type: Union[Type["ContractEvent"], Type["AsyncContractEvent"]], address: Optional[ChecksumAddress] = None, ) -> None: - if abi: - self.abi = abi - self._events = filter_abi_by_type("event", self.abi) - for event in self._events: - setattr( - self, - event["name"], - contract_event_type.factory( - event["name"], - w3=w3, - contract_abi=self.abi, - address=address, - event_name=event["name"], - abi=event, - ), + # Keep a copy of each attribute to prevent variable collisions with + # contract event names + _abi = abi + _w3 = w3 + _address = address + _events: Sequence[ABIEvent] = None + + if _abi: + _events = filter_abi_by_type("event", _abi) + for event in _events: + abi_signature = abi_to_signature(event) + event_factory = contract_event_type.factory( + abi_signature, + w3=_w3, + contract_abi=_abi, + address=_address, + event_name=event["name"], ) + setattr(self, abi_signature, event_factory) - def __getattr__(self, event_name: str) -> Type["BaseContractEvent"]: - if "_events" not in self.__dict__: - raise NoABIEventsFound( - "The abi for this contract contains no event definitions. ", - "Are you sure you provided the correct contract abi?", - ) - elif event_name not in self.__dict__["_events"]: - raise ABIEventNotFound( - f"The event '{event_name}' was not found in this contract's abi. ", - "Are you sure you provided the correct contract abi?", - ) - else: - return super().__getattribute__(event_name) - - def __getitem__(self, event_name: str) -> Type["BaseContractEvent"]: - return getattr(self, event_name) + if _events: + self._events = _events - def __iter__(self) -> Iterable[Type["BaseContractEvent"]]: - """ - Iterate over supported - - :return: Iterable of :class:`ContractEvent` - """ - for event in self._events: - yield self[event["name"]] + self.abi = _abi + self.w3 = _w3 + self.address = _address def __hasattr__(self, event_name: str) -> bool: try: @@ -505,41 +495,58 @@ class BaseContractFunction: kwargs: Any = None def __init__(self, abi: Optional[ABIFunction] = None) -> None: - self.abi = abi - self.fn_name = type(self).__name__ + if not self.abi_element_identifier: + self.abi_element_identifier = type(self).__name__ + + self.fn_name = get_name_from_abi_element_identifier(self.abi_element_identifier) + self.abi = cast( + ABIFunction, + get_abi_element( + filter_by_types( + ["function", "constructor", "fallback", "receive"], + self.contract_abi, + ), + self.abi_element_identifier, + ), + ) - def _set_function_info(self) -> None: - if not self.abi: - self.abi = cast( + @combomethod + def _get_abi(cls) -> ABIFunction: + if not cls.args and not cls.kwargs: + # If no args or kwargs are provided, get the ABI element by name + return cast( ABIFunction, get_abi_element( - self.contract_abi, - self.abi_element_identifier, - *self.args, - abi_codec=self.w3.codec, - **self.kwargs, + cls.contract_abi, + get_abi_element_signature(cls.abi_element_identifier), + abi_codec=cls.w3.codec, ), ) - if self.abi_element_identifier in [ - FallbackFn, - ReceiveFn, - ]: - self.selector = encode_hex(b"") - elif is_text(self.abi_element_identifier): - self.selector = encode_hex(function_abi_to_4byte_selector(self.abi)) - else: - raise Web3TypeError("Unsupported function identifier") + return cast( + ABIFunction, + get_abi_element( + cls.contract_abi, + get_name_from_abi_element_identifier(cls.abi_element_identifier), + *cls.args, + abi_codec=cls.w3.codec, + **cls.kwargs, + ), + ) - if self.abi_element_identifier in [ - FallbackFn, - ReceiveFn, - ]: + def _set_function_info(self) -> None: + self.selector = encode_hex(b"") + if self.abi_element_identifier in [FallbackFn, ReceiveFn]: self.arguments = None - else: + elif is_text(self.abi_element_identifier): + self.abi = self._get_abi() + + self.selector = encode_hex(function_abi_to_4byte_selector(self.abi)) self.arguments = get_normalized_abi_inputs( self.abi, *self.args, **self.kwargs ) + else: + raise Web3TypeError("Unsupported function identifier") def _get_call_txparams(self, transaction: Optional[TxParams] = None) -> TxParams: if transaction is None: @@ -671,7 +678,7 @@ def __repr__(self) -> str: if self.arguments is not None: _repr += f" bound to {self.arguments!r}" return _repr + ">" - return f"" + return f"" @classmethod def factory( @@ -685,6 +692,8 @@ def factory( class BaseContractFunctions: """Class containing contract function objects""" + _functions: Sequence[ABIFunction] = None + def __init__( self, abi: ABI, @@ -695,35 +704,35 @@ def __init__( address: Optional[ChecksumAddress] = None, decode_tuples: Optional[bool] = False, ) -> None: - self.abi = abi - self.w3 = w3 - self.address = address - - if self.abi: - self._functions = filter_abi_by_type("function", self.abi) - for func in self._functions: + # Keep a copy of each attribute to prevent variable collisions with + # contract function names + _abi = abi + _w3 = w3 + _address = address + _functions: Sequence[ABIFunction] = None + + if _abi: + _functions = filter_abi_by_type("function", _abi) + for func in _functions: + abi_signature = abi_to_signature(func) setattr( self, - func["name"], + abi_signature, contract_function_class.factory( - func["name"], - w3=self.w3, - contract_abi=self.abi, - address=self.address, + abi_signature, + w3=_w3, + contract_abi=_abi, + address=_address, decode_tuples=decode_tuples, - abi_element_identifier=func["name"], ), ) - def __iter__(self) -> Iterable["ABIFunction"]: - if not hasattr(self, "_functions") or not self._functions: - return - - for func in self._functions: - yield self[func["name"]] + if _functions: + self._functions = _functions - def __getitem__(self, function_name: str) -> ABIFunction: - return getattr(self, function_name) + self.abi = _abi + self.w3 = _w3 + self.address = _address def __hasattr__(self, function_name: str) -> bool: try: @@ -813,7 +822,7 @@ def encode_abi( @combomethod def all_functions( self, - ) -> "BaseContractFunction": + ) -> List["BaseContractFunction"]: """ Return all functions in the contract. """ @@ -843,7 +852,7 @@ def callable_check(fn_abi: ABIFunction) -> bool: return self.get_function_by_identifier(fns, "signature") @combomethod - def find_functions_by_name(self, fn_name: str) -> "BaseContractFunction": + def find_functions_by_name(self, fn_name: str) -> List["BaseContractFunction"]: """ Return all functions with matching name. Raises a Web3ValueError if there is no match or more than one is found. @@ -1138,6 +1147,9 @@ def _find_matching_fn_abi( *args: Sequence[Any], **kwargs: Dict[str, Any], ) -> ABIElement: + if not args and not kwargs: + fn_identifier = get_abi_element_signature(fn_identifier) + return get_abi_element( cls.abi, fn_identifier, @@ -1152,8 +1164,13 @@ def _get_event_abi( event_name: Optional[str] = None, argument_names: Optional[Sequence[str]] = None, ) -> ABIEvent: - return get_event_abi( - abi=cls.abi, event_name=event_name, argument_names=argument_names + return cast( + ABIEvent, + get_abi_element( + abi=cls.abi, + abi_element_identifier=event_name, + argument_names=argument_names, + ), ) @combomethod @@ -1219,7 +1236,9 @@ def __init__( def __getattr__(self, function_name: str) -> Any: function_names = [ - fn["name"] for fn in self._functions if fn.get("type") == "function" + get_name_from_abi_element_identifier(fn["name"]) + for fn in self._functions + if fn.get("type") == "function" ] if self.abi is None: raise NoABIFound( @@ -1230,7 +1249,7 @@ def __getattr__(self, function_name: str) -> Any: "The ABI for this contract contains no function definitions. ", "Are you sure you provided the correct contract ABI?", ) - elif function_name not in function_names: + elif get_name_from_abi_element_identifier(function_name) not in function_names: functions_available = ", ".join(function_names) raise ABIFunctionNotFound( f"The function '{function_name}' was not found in this contract's ABI.", @@ -1239,11 +1258,17 @@ def __getattr__(self, function_name: str) -> Any: "Did you mean to call one of those functions?", ) else: - return super().__getattribute__(function_name) + function_identifier = function_name - def __hasattr__(self, event_name: str) -> bool: + if "(" not in function_name: + function_identifier = _get_any_abi_signature_with_name( + function_name, self._functions + ) + return super().__getattribute__(function_identifier) + + def __hasattr__(self, function_name: str) -> bool: try: - return event_name in self.__dict__["_events"] + return function_name in self.__dict__["_functions"] except ABIFunctionNotFound: return False diff --git a/web3/contract/contract.py b/web3/contract/contract.py index c564a148eb..e3a623ec5e 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -1,4 +1,3 @@ -import copy from typing import ( TYPE_CHECKING, Any, @@ -18,8 +17,10 @@ ChecksumAddress, ) from eth_utils import ( + abi_to_signature, combomethod, get_abi_input_names, + get_abi_input_types, get_all_function_abis, ) from eth_utils.toolz import ( @@ -31,6 +32,8 @@ from web3._utils.abi import ( fallback_func_abi_exists, + get_abi_element_signature, + get_name_from_abi_element_identifier, receive_func_abi_exists, ) from web3._utils.abi_element_identifiers import ( @@ -41,6 +44,8 @@ Self, ) from web3._utils.contracts import ( + copy_contract_event, + copy_contract_function, parse_block_identifier, ) from web3._utils.datatypes import ( @@ -83,7 +88,9 @@ transact_with_contract_function, ) from web3.exceptions import ( + ABIEventNotFound, ABIFunctionNotFound, + NoABIEventsFound, NoABIFound, NoABIFunctionsFound, Web3AttributeError, @@ -98,6 +105,7 @@ TxParams, ) from web3.utils.abi import ( + _get_any_abi_signature_with_name, get_abi_element, ) @@ -110,16 +118,8 @@ class ContractEvent(BaseContractEvent): # mypy types w3: "Web3" - def __call__(self) -> "ContractEvent": - clone = copy.copy(self) - - if not self.abi: - self.abi = cast( - ABIEvent, - get_abi_element(self.contract_abi, self.event_name), - ) - - return clone + def __call__(self, *args: Any, **kwargs: Any) -> "ContractEvent": + return copy_contract_event(self, *args, **kwargs) @combomethod def get_logs( @@ -186,7 +186,6 @@ def get_logs( :yield: Tuple of :class:`AttributeDict` instances """ event_abi = self._get_event_abi() - # validate ``argument_filters`` if present if argument_filters is not None: event_arg_names = get_abi_input_names(event_abi) @@ -228,7 +227,8 @@ def create_filter( """ Create filter object that tracks logs emitted by this contract event. """ - filter_builder = EventFilterBuilder(self._get_event_abi(), self.w3.codec) + abi = self._get_event_abi() + filter_builder = EventFilterBuilder(abi, self.w3.codec) self._set_up_filter_builder( argument_filters, from_block, @@ -238,19 +238,18 @@ def create_filter( filter_builder, ) log_filter = filter_builder.deploy(self.w3) - log_filter.log_entry_formatter = get_event_data( - self.w3.codec, self._get_event_abi() - ) + log_filter.log_entry_formatter = get_event_data(self.w3.codec, abi) log_filter.builder = filter_builder return log_filter @combomethod def build_filter(self) -> EventFilterBuilder: + abi = self._get_event_abi() builder = EventFilterBuilder( - self._get_event_abi(), + abi, self.w3.codec, - formatter=get_event_data(self.w3.codec, self._get_event_abi()), + formatter=get_event_data(self.w3.codec, abi), ) builder.address = self.address return builder @@ -268,24 +267,44 @@ def __init__( ) -> None: super().__init__(abi, w3, ContractEvent, address) + def __getattr__(self, event_name: str) -> "ContractEvent": + if super().__getattribute__("abi") is None: + raise NoABIFound( + "There is no ABI found for this contract.", + ) + if "_events" not in self.__dict__: + raise NoABIEventsFound( + "The abi for this contract contains no event definitions. ", + "Are you sure you provided the correct contract abi?", + ) + elif get_name_from_abi_element_identifier(event_name) not in [ + get_name_from_abi_element_identifier(event["name"]) + for event in self._events + ]: + raise ABIEventNotFound( + f"The event '{event_name}' was not found in this contract's abi. ", + "Are you sure you provided the correct contract abi?", + ) + else: + event_abi = get_abi_element(self._events, event_name) + argument_types = get_abi_input_types(event_abi) + event_signature = str(get_abi_element_signature(event_name, argument_types)) + return super().__getattribute__(event_signature) + + def __getitem__(self, event_name: str) -> "ContractEvent": + return getattr(self, event_name) + + def __iter__(self) -> Iterable["ContractEvent"]: + for event in self._events: + yield self[event["name"]] + class ContractFunction(BaseContractFunction): # mypy types w3: "Web3" def __call__(self, *args: Any, **kwargs: Any) -> "ContractFunction": - clone = copy.copy(self) - if args is None: - clone.args = tuple() - else: - clone.args = args - - if kwargs is None: - clone.kwargs = {} - else: - clone.kwargs = kwargs - clone._set_function_info() - return clone + return copy_contract_function(self, *args, **kwargs) @classmethod def factory(cls, class_name: str, **kwargs: Any) -> Self: @@ -329,11 +348,13 @@ def call( block_id = parse_block_identifier(self.w3, block_identifier) + abi_element_identifier = abi_to_signature(self.abi) + return call_contract_function( self.w3, self.address, self._return_data_normalizers, - self.abi_element_identifier, + abi_element_identifier, call_transaction, block_id, self.contract_abi, @@ -347,11 +368,12 @@ def call( def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: setup_transaction = self._transact(transaction) + abi_element_identifier = abi_to_signature(self.abi) return transact_with_contract_function( self.address, self.w3, - self.abi_element_identifier, + abi_element_identifier, setup_transaction, self.contract_abi, self.abi, @@ -366,10 +388,11 @@ def estimate_gas( state_override: Optional[StateOverride] = None, ) -> int: setup_transaction = self._estimate_gas(transaction) + abi_element_identifier = abi_to_signature(self.abi) return estimate_gas_for_function( self.address, self.w3, - self.abi_element_identifier, + abi_element_identifier, setup_transaction, self.contract_abi, self.abi, @@ -381,11 +404,12 @@ def estimate_gas( def build_transaction(self, transaction: Optional[TxParams] = None) -> TxParams: built_transaction = self._build_transaction(transaction) + abi_element_identifier = abi_to_signature(self.abi) return build_transaction_for_function( self.address, self.w3, - self.abi_element_identifier, + abi_element_identifier, built_transaction, self.contract_abi, self.abi, @@ -436,23 +460,60 @@ def __init__( ) -> None: super().__init__(abi, w3, ContractFunction, address, decode_tuples) - def __getattr__(self, function_name: str) -> "ContractFunction": - if self.abi is None: + def __iter__(self) -> Iterable["ContractFunction"]: + if not hasattr(self, "_functions") or not self._functions: + return + + for func in self._functions: + yield self[abi_to_signature(func)] + + def __getattribute__(self, function_name: str) -> "ContractFunction": + function_identifier = function_name + + # Function names can override object attributes + if function_name in ["abi", "w3", "address"] and super().__getattribute__( + "_functions" + ): + function_identifier = _get_any_abi_signature_with_name( + function_name, super().__getattribute__("_functions") + ) + + return super().__getattribute__(function_identifier) + + def __getattr__( + self, function_name: str + ) -> Callable[[Any, Any], "ContractFunction"]: + if super().__getattribute__("abi") is None: raise NoABIFound( "There is no ABI found for this contract.", ) - if "_functions" not in self.__dict__: + elif "_functions" not in self.__dict__: raise NoABIFunctionsFound( "The abi for this contract contains no function definitions. ", "Are you sure you provided the correct contract abi?", ) - elif function_name not in self.__dict__["_functions"]: + elif get_name_from_abi_element_identifier(function_name) not in [ + get_name_from_abi_element_identifier(function["name"]) + for function in self._functions + ]: raise ABIFunctionNotFound( - f"The function '{function_name}' was not found in this contract's abi.", - " Are you sure you provided the correct contract abi?", + f"The function '{function_name}' was not found in this contract's " + "abi. Are you sure you provided the correct contract abi?", ) - else: - return super().__getattribute__(function_name) + + function_identifier = function_name + + if "(" not in function_name: + function_identifier = _get_any_abi_signature_with_name( + function_name, self._functions + ) + + return super().__getattribute__( + function_identifier, + ) + + def __getitem__(self, function_name: str) -> "ContractFunction": + return getattr(self, function_name) class Contract(BaseContract): @@ -627,12 +688,12 @@ def __init__( self._functions = get_all_function_abis(self.abi) for func in self._functions: + abi_signature = abi_to_signature(func) fn = ContractFunction.factory( - func["name"], + abi_signature, w3=w3, contract_abi=self.abi, address=self.address, - abi_element_identifier=func["name"], decode_tuples=decode_tuples, ) @@ -644,7 +705,7 @@ def __init__( ccip_read_enabled=ccip_read_enabled, ) - setattr(self, func["name"], caller_method) + setattr(self, abi_signature, caller_method) def __call__( self, diff --git a/web3/contract/utils.py b/web3/contract/utils.py index b64bc632f9..1fb1a74f57 100644 --- a/web3/contract/utils.py +++ b/web3/contract/utils.py @@ -23,6 +23,7 @@ TypeStr, ) from eth_utils.abi import ( + abi_to_signature, filter_abi_by_type, get_abi_output_types, ) @@ -345,11 +346,11 @@ def find_functions_by_identifier( fns_abi = filter_abi_by_type("function", contract_abi) return [ function_type.factory( - fn_abi["name"], + abi_to_signature(fn_abi), w3=w3, contract_abi=contract_abi, address=address, - abi_element_identifier=fn_abi["name"], + abi_element_identifier=abi_to_signature(fn_abi), abi=fn_abi, ) for fn_abi in fns_abi diff --git a/web3/utils/abi.py b/web3/utils/abi.py index d76f8fc7a1..8ac924c8d4 100644 --- a/web3/utils/abi.py +++ b/web3/utils/abi.py @@ -1,6 +1,7 @@ import functools from typing import ( Any, + Callable, Dict, List, Optional, @@ -47,7 +48,6 @@ ) from eth_utils.types import ( is_list_like, - is_text, ) from hexbytes import ( HexBytes, @@ -55,17 +55,21 @@ from web3._utils.abi import ( filter_by_argument_name, + filter_by_argument_type, + get_abi_element_signature, + get_name_from_abi_element_identifier, ) -from web3._utils.abi_element_identifiers import ( - FallbackFn, - ReceiveFn, +from web3._utils.decorators import ( + deprecated_for, +) +from web3._utils.validation import ( + validate_abi, ) from web3.exceptions import ( ABIConstructorNotFound, ABIFallbackNotFound, ABIReceiveNotFound, MismatchedABI, - Web3TypeError, Web3ValidationError, Web3ValueError, ) @@ -81,9 +85,14 @@ function_abi_to_4byte_selector, get_aligned_abi_inputs, get_normalized_abi_inputs, + get_abi_input_types, ) +def _filter_by_signature(signature: str, contract_abi: ABI) -> List[ABIElement]: + return [abi for abi in contract_abi if abi_to_signature(abi) == signature] + + def _filter_by_argument_count( num_arguments: int, contract_abi: ABI ) -> List[ABIElement]: @@ -98,9 +107,9 @@ def _filter_by_argument_count( def _filter_by_encodability( abi_codec: codec.ABIEncoder, + args: Sequence[Any], + kwargs: Dict[str, Any], contract_abi: ABI, - *args: Optional[Sequence[Any]], - **kwargs: Optional[Dict[str, Any]], ) -> List[ABICallable]: return [ cast(ABICallable, function_abi) @@ -158,13 +167,119 @@ def _get_fallback_function_abi(contract_abi: ABI) -> ABIFallback: raise ABIFallbackNotFound("No fallback function was found in the contract ABI.") +def _get_any_abi_signature_with_name(element_name: str, contract_abi: ABI) -> str: + """ + Find an ABI identifier signature by element name. A signature identifier is + returned, "name(arg1Type,arg2Type,...)". + + This function forces a result to be returned even if multiple are found. Depending + on the ABIs found, the signature may be returned with or without arguments types. + The signature without arguments is returned when there is a matching ABI with no + arguments. + """ + try: + # search for function abis with the same name + function_abi = get_abi_element( + contract_abi, get_name_from_abi_element_identifier(element_name) + ) + return abi_to_signature(function_abi) + except MismatchedABI: + # If all matching functions have arguments, cannot determine which one + # to use. Instead of an exception, return the first matching function. + function_abis = filter_abi_by_name(element_name, contract_abi) + if len(function_abis) > 0 and all( + len(get_abi_input_types(fn)) > 0 for fn in function_abis + ): + return abi_to_signature(function_abis[0]) + + # Use signature for function that does not take arguments + return str(get_abi_element_signature(element_name)) + + +def _build_abi_input_error( + abi: ABI, + num_args: int, + *args: Any, + abi_codec: ABICodec, + **kwargs: Any, +) -> str: + """ + Build a string representation of the ABI input error. + """ + errors: Dict[str, str] = dict( + { + "zero_args": "", + "invalid_args": "", + "encoding": "", + "unexpected_args": "", + } + ) + + for abi_element in abi: + abi_element_input_types = get_abi_input_types(abi_element) + abi_signature = abi_to_signature(abi_element) + abi_element_name = get_name_from_abi_element_identifier(abi_signature) + types: Tuple[str, ...] = tuple() + aligned_args: Tuple[Any, ...] = tuple() + + if len(abi_element_input_types) == num_args: + if num_args == 0: + if not errors["zero_args"]: + errors["zero_args"] += ( + "The provided identifier matches multiple elements.\n" + f"If you meant to call `{abi_element_name}()`, " + "please specify the full signature.\n" + ) + + errors["zero_args"] += ( + f" - signature: {abi_to_signature(abi_element)}, " + f"type: {abi_element['type']}\n" + ) + else: + try: + arguments = get_normalized_abi_inputs(abi_element, *args, **kwargs) + types, aligned_args = get_aligned_abi_inputs(abi_element, arguments) + except TypeError as e: + errors["invalid_args"] += ( + f"Signature: {abi_signature}, type: {abi_element['type']}\n" + f"Arguments do not match types in `{abi_signature}`.\n" + f"Error: {e}\n" + ) + + argument_errors = "" + for position, (_type, arg) in enumerate(zip(types, aligned_args), start=1): + if abi_codec.is_encodable(_type, arg): + argument_errors += f"Argument {position} value `{arg}` is valid.\n" + else: + argument_errors += ( + f"Argument {position} value `{arg}` is not compatible with " + f"type `{_type}`.\n" + ) + + if argument_errors != "": + errors["encoding"] += ( + f"Signature: {abi_signature}, type: {abi_element['type']}\n" + + argument_errors + ) + + else: + errors["unexpected_args"] += ( + f"Signature: {abi_signature}, type: {abi_element['type']}\n" + f"Expected {len(abi_element_input_types)} argument(s) but received " + f"{num_args} argument(s).\n" + ) + + return "".join(errors.values()) + + def _mismatched_abi_error_diagnosis( abi_element_identifier: ABIElementIdentifier, - matching_function_signatures: Sequence[str], - arg_count_matches: int, - encoding_matches: int, - *args: Optional[Sequence[Any]], - **kwargs: Optional[Dict[str, Any]], + abi: ABI, + num_matches: int = 0, + num_args: int = 0, + *args: Optional[Any], + abi_codec: Optional[Any] = None, + **kwargs: Optional[Any], ) -> str: """ Raise a ``MismatchedABI`` when a function ABI lookup results in an error. @@ -172,32 +287,59 @@ def _mismatched_abi_error_diagnosis( An error may result from multiple functions matching the provided signature and arguments or no functions are identified. """ - diagnosis = "\n" - if arg_count_matches == 0: - diagnosis += "Function invocation failed due to improper number of arguments." - elif encoding_matches == 0: - diagnosis += "Function invocation failed due to no matching argument types." - elif encoding_matches > 1: - diagnosis += ( - "Ambiguous argument encoding. " - "Provided arguments can be encoded to multiple functions " - "matching this call." - ) + name = get_name_from_abi_element_identifier(abi_element_identifier) + abis_matching_names = filter_abi_by_name(name, abi) + abis_matching_arg_count = [ + abi_to_signature(abi) + for abi in _filter_by_argument_count(num_args, abis_matching_names) + ] + num_abis_matching_arg_count = len(abis_matching_arg_count) - collapsed_args = _extract_argument_types(*args) - collapsed_kwargs = dict( - {(k, _extract_argument_types([v])) for k, v in kwargs.items()} - ) + if abi_codec is None: + abi_codec = ABICodec(default_registry) - return ( - f"\nCould not identify the intended function with name " - f"`{abi_element_identifier}`, positional arguments with type(s) " - f"`({collapsed_args})` and keyword arguments with type(s) " - f"`{collapsed_kwargs}`." - f"\nFound {len(matching_function_signatures)} function(s) with the name " - f"`{abi_element_identifier}`: {matching_function_signatures}{diagnosis}" + error = "ABI Not Found!\n" + if num_matches == 0 and num_abis_matching_arg_count == 0: + error += f"No element named `{name}` with {num_args} argument(s).\n" + elif num_matches > 1 or num_abis_matching_arg_count > 1: + error += ( + f"Found multiple elements named `{name}` that accept {num_args} " + "argument(s).\n" + ) + elif num_abis_matching_arg_count == 1: + error += ( + f"Found {num_abis_matching_arg_count} element(s) named `{name}` that " + f"accept {num_args} argument(s).\n" + "The provided arguments are not valid.\n" + ) + elif num_matches == 0: + error += ( + f"Unable to find an element named `{name}` that matches the provided " + "identifier and argument types.\n" + ) + arg_types = _extract_argument_types(*args) + kwarg_types = dict({(k, _extract_argument_types([v])) for k, v in kwargs.items()}) + error += ( + f"Provided argument types: ({arg_types})\n" + f"Provided keyword argument types: {kwarg_types}\n\n" ) + if abis_matching_names: + error += ( + f"Tried to find a matching ABI element named `{name}`, but encountered " + "the following problems:\n" + ) + + error += _build_abi_input_error( + abis_matching_names, + num_args, + *args, + abi_codec=abi_codec, + **kwargs, + ) + + return f"\n{error}" + def _extract_argument_types(*args: Sequence[Any]) -> str: """ @@ -231,6 +373,102 @@ def _get_argument_readable_type(arg: Any) -> str: return arg.__class__.__name__ +def _build_abi_filters( + abi_element_identifier: ABIElementIdentifier, + *args: Optional[Any], + abi_type: Optional[str] = None, + argument_names: Optional[Sequence[str]] = None, + argument_types: Optional[Sequence[str]] = None, + abi_codec: Optional[Any] = None, + **kwargs: Optional[Any], +) -> List[Callable[..., Sequence[ABIElement]]]: + """ + Build a list of ABI filters to find an ABI element within a contract ABI. Each + filter is a partial function that takes a contract ABI and returns a filtered list. + Each parameter is checked before applying the relevant filter. + + When the ``abi_element_identifier`` is a function name or signature and no arguments + are provided, the returned filters include the function name or signature. + + A function ABI may take arguments and keyword arguments. When the ``args`` and + ``kwargs`` values are passed, several filters are combined together. Available + filters include the function name, argument count, argument name, argument type, + and argument encodability. + + ``constructor``, ``fallback``, and ``receive`` ABI elements are handled only with a + filter by type. + """ + if not isinstance(abi_element_identifier, str): + abi_element_identifier = get_abi_element_signature(abi_element_identifier) + + if abi_element_identifier in ["constructor", "fallback", "receive"]: + return [functools.partial(filter_abi_by_type, abi_element_identifier)] + + filters: List[Callable[..., Sequence[ABIElement]]] = [] + + if abi_type: + filters.append(functools.partial(filter_abi_by_type, abi_type)) + + arg_count = 0 + if argument_names: + arg_count = len(argument_names) + elif args or kwargs: + arg_count = len(args) + len(kwargs) + + if arg_count > 0: + filters.append( + functools.partial( + filter_abi_by_name, + get_name_from_abi_element_identifier(abi_element_identifier), + ) + ) + filters.append(functools.partial(_filter_by_argument_count, arg_count)) + + if args or kwargs: + if abi_codec is None: + abi_codec = ABICodec(default_registry) + + filters.append( + functools.partial( + _filter_by_encodability, + abi_codec, + args, + kwargs, + ) + ) + + if argument_names: + filters.append(functools.partial(filter_by_argument_name, argument_names)) + + if argument_types: + if arg_count != len(argument_types): + raise Web3ValidationError( + "The number of argument names and types must match." + ) + + filters.append( + functools.partial(filter_by_argument_type, argument_types) + ) + + if "(" in abi_element_identifier: + filters.append( + functools.partial(_filter_by_signature, abi_element_identifier) + ) + else: + filters.append( + functools.partial( + filter_abi_by_name, + get_name_from_abi_element_identifier(abi_element_identifier), + ) + ) + if "(" in abi_element_identifier: + filters.append( + functools.partial(_filter_by_signature, abi_element_identifier) + ) + + return filters + + def get_abi_element_info( abi: ABI, abi_element_identifier: ABIElementIdentifier, @@ -306,16 +544,17 @@ def get_abi_element_info( def get_abi_element( abi: ABI, abi_element_identifier: ABIElementIdentifier, - *args: Optional[Sequence[Any]], + *args: Optional[Any], abi_codec: Optional[Any] = None, - **kwargs: Optional[Dict[str, Any]], + **kwargs: Optional[Any], ) -> ABIElement: """ - Return the interface for an ``ABIElement`` which matches the provided identifier - and arguments. + Return the interface for an ``ABIElement`` from the ``abi`` that matches the + provided identifier and arguments. - The ABI which matches the provided identifier, named arguments (``args``) and - keyword args (``kwargs``) will be returned. + The ``ABIElementIdentifier`` value may be a function name, signature, or a + ``FallbackFn`` or ``ReceiveFn``. When named arguments (``args``) and/or keyword args + (``kwargs``) are provided, they are included in the search filters. The `abi_codec` may be overridden if custom encoding and decoding is required. The default is used if no codec is provided. More details about customizations are in @@ -323,7 +562,9 @@ def get_abi_element( :param abi: Contract ABI. :type abi: `ABI` - :param abi_element_identifier: Find an element ABI with matching identifier. + :param abi_element_identifier: Find an element ABI with matching identifier. The \ + identifier may be a function name, signature, or ``FallbackFn`` or ``ReceiveFn``. \ + A function signature is in the form ``name(arg1Type,arg2Type,...)``. :type abi_element_identifier: `ABIElementIdentifier` :param args: Find an element ABI with matching args. :type args: `Optional[Sequence[Any]]` @@ -358,55 +599,38 @@ def get_abi_element( 'type': 'uint256'}], 'payable': False, 'stateMutability': 'nonpayable', \ 'type': 'function'} """ + validate_abi(abi) + if abi_codec is None: abi_codec = ABICodec(default_registry) - if abi_element_identifier is FallbackFn or abi_element_identifier == "fallback": - return _get_fallback_function_abi(abi) - - if abi_element_identifier is ReceiveFn or abi_element_identifier == "receive": - return _get_receive_function_abi(abi) - - if abi_element_identifier is None or not is_text(abi_element_identifier): - raise Web3TypeError("Unsupported function identifier") - - filtered_abis_by_name: Sequence[ABIElement] - if abi_element_identifier == "constructor": - filtered_abis_by_name = [_get_constructor_function_abi(abi)] - else: - filtered_abis_by_name = filter_abi_by_name( - cast(str, abi_element_identifier), abi - ) - - arg_count = len(args) + len(kwargs) - filtered_abis_by_arg_count = _filter_by_argument_count( - arg_count, filtered_abis_by_name - ) - - if not args and not kwargs and len(filtered_abis_by_arg_count) == 1: - return filtered_abis_by_arg_count[0] - - elements_with_encodable_args = _filter_by_encodability( - abi_codec, filtered_abis_by_arg_count, *args, **kwargs + abi_element_matches: Sequence[ABIElement] = pipe( + abi, + *_build_abi_filters( + abi_element_identifier, + *args, + abi_codec=abi_codec, + **kwargs, + ), ) - if len(elements_with_encodable_args) != 1: - matching_function_signatures = [ - abi_to_signature(func) for func in filtered_abis_by_name - ] + num_matches = len(abi_element_matches) + # Raise MismatchedABI when more than one found + if num_matches != 1: error_diagnosis = _mismatched_abi_error_diagnosis( abi_element_identifier, - matching_function_signatures, - len(filtered_abis_by_arg_count), - len(elements_with_encodable_args), + abi, + num_matches, + len(args) + len(kwargs), *args, + abi_codec=abi_codec, **kwargs, ) raise MismatchedABI(error_diagnosis) - return elements_with_encodable_args[0] + return abi_element_matches[0] def check_if_arguments_can_be_encoded( @@ -472,12 +696,17 @@ def check_if_arguments_can_be_encoded( ) +@deprecated_for("get_abi_element") def get_event_abi( abi: ABI, event_name: str, argument_names: Optional[Sequence[str]] = None, ) -> ABIEvent: """ + .. warning:: + This function is deprecated. It is unable to distinguish between + overloaded events. Use ``get_abi_element`` instead. + Find the event interface with the given name and/or arguments. :param abi: Contract ABI. From 5dc34fd7c2854d20eaf46b76e3c23a00151dc63f Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Thu, 24 Oct 2024 17:02:34 -0600 Subject: [PATCH 2/8] Initialize contract with ambiguous events or functions --- .../test_contract_class_construction.py | 99 +++------------- .../contract_data/event_contracts.py | 110 +++++++++++++++++- .../function_name_tester_contract.py | 19 ++- web3/contract/async_contract.py | 28 ++--- web3/contract/base_contract.py | 53 ++++----- web3/contract/contract.py | 32 +++-- 6 files changed, 184 insertions(+), 157 deletions(-) diff --git a/tests/core/contracts/test_contract_class_construction.py b/tests/core/contracts/test_contract_class_construction.py index 2c9c7d3b92..af8c992d27 100644 --- a/tests/core/contracts/test_contract_class_construction.py +++ b/tests/core/contracts/test_contract_class_construction.py @@ -48,94 +48,23 @@ def test_abi_as_json_string(w3, math_contract_abi, some_address): assert math.abi == math_contract_abi -def test_contract_init_with_abi_function_name( - w3, - function_name_tester_contract_abi, - function_name_tester_contract, -): - # test `abi` function name does not throw when creating the contract factory - contract_factory = w3.eth.contract(abi=function_name_tester_contract_abi) - - # re-instantiate the contract - contract = contract_factory(function_name_tester_contract.address) - - # Contract `abi`` function should not override `web3` attribute - assert contract.abi == function_name_tester_contract_abi - - with pytest.raises(TypeError): - contract.functions.abi[0] - - # assert the `abi` function returns true when called - result = contract.functions.abi().call() - assert result is True - - -@pytest.mark.asyncio -async def test_async_contract_init_with_abi_function_name( - async_w3, - function_name_tester_contract_abi, - async_function_name_tester_contract, -): - # test `abi` function name does not throw when creating the contract factory - contract_factory = async_w3.eth.contract(abi=function_name_tester_contract_abi) - - # re-instantiate the contract - contract = contract_factory(async_function_name_tester_contract.address) - - # Contract `abi` function should not override `web3` attribute - assert contract.abi == function_name_tester_contract_abi - - with pytest.raises(TypeError): - contract.functions.abi[0] - - # assert the `abi` function returns true when called - result = await contract.functions.abi().call() - assert result is True - - -def test_contract_init_with_w3_function_name( - w3, - function_name_tester_contract_abi, - function_name_tester_contract, -): - # test `w3` function name does not throw when creating the contract factory - contract_factory = w3.eth.contract(abi=function_name_tester_contract_abi) - - # re-instantiate the contract - contract = contract_factory(function_name_tester_contract.address) - - # Contract w3 function should not override web3 instance - with pytest.raises(AttributeError): - contract.functions.w3.eth.get_block("latest") - - assert contract.w3.eth.get_block("latest") is not None - - # assert the `w3` function returns true when called - result = contract.functions.w3().call() - assert result is True +@pytest.mark.parametrize( + "abi", + ([{"type": "function", "name": "abi"}], [{"type": "function", "name": "address"}]), +) +def test_contract_init_with_reserved_name(w3, abi): + with pytest.raises(Web3AttributeError): + w3.eth.contract(abi=abi) @pytest.mark.asyncio -async def test_async_contract_init_with_w3_function_name( - async_w3, - function_name_tester_contract_abi, - async_function_name_tester_contract, -): - # test `w3` function name does not throw when creating the contract factory - contract_factory = async_w3.eth.contract(abi=function_name_tester_contract_abi) - - # re-instantiate the contract - contract = contract_factory(async_function_name_tester_contract.address) - - # Contract w3 function should not override web3 instance - with pytest.raises(AttributeError): - contract.functions.w3.eth.get_block("latest") - - assert contract.w3.eth.get_block("latest") is not None - - # assert the `w3` function returns true when called - result = await contract.functions.w3().call() - assert result is True +@pytest.mark.parametrize( + "abi", + ([{"type": "function", "name": "abi"}], [{"type": "function", "name": "address"}]), +) +async def test_async_contract_init_with_reserved_name(async_w3, abi): + with pytest.raises(Web3AttributeError): + await async_w3.eth.contract(abi=abi) def test_error_to_call_non_existent_fallback( diff --git a/web3/_utils/contract_sources/contract_data/event_contracts.py b/web3/_utils/contract_sources/contract_data/event_contracts.py index 0a168640e0..c3961f6ea1 100644 --- a/web3/_utils/contract_sources/contract_data/event_contracts.py +++ b/web3/_utils/contract_sources/contract_data/event_contracts.py @@ -6,7 +6,41 @@ # source: web3/_utils/contract_sources/EventContracts.sol:EventContract EVENT_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b5061017a8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100f1565b610049565b005b7ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1581604051610078919061012b565b60405180910390a17f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100af919061012b565b60405180910390a150565b5f5ffd5b5f819050919050565b6100d0816100be565b81146100da575f5ffd5b50565b5f813590506100eb816100c7565b92915050565b5f60208284031215610106576101056100ba565b5b5f610113848285016100dd565b91505092915050565b610125816100be565b82525050565b5f60208201905061013e5f83018461011c565b9291505056fea2646970667358221220b0bcf8d91a48b57e8c3a13a2358baf5a199c99a22aebe7940a1dc0397f284b9064736f6c634300081b0033" # noqa: E501 EVENT_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100f1565b610049565b005b7ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1581604051610078919061012b565b60405180910390a17f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100af919061012b565b60405180910390a150565b5f5ffd5b5f819050919050565b6100d0816100be565b81146100da575f5ffd5b50565b5f813590506100eb816100c7565b92915050565b5f60208284031215610106576101056100ba565b5b5f610113848285016100dd565b91505092915050565b610125816100be565b82525050565b5f60208201905061013e5f83018461011c565b9291505056fea2646970667358221220b0bcf8d91a48b57e8c3a13a2358baf5a199c99a22aebe7940a1dc0397f284b9064736f6c634300081b0033" # noqa: E501 -EVENT_CONTRACT_ABI = [{'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleArg', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleWithIndex', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '_arg0', 'type': 'uint256'}], 'name': 'logTwoEvents', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}] +EVENT_CONTRACT_ABI = [ + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleArg", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleWithIndex", + "type": "event", + }, + { + "inputs": [{"internalType": "uint256", "name": "_arg0", "type": "uint256"}], + "name": "logTwoEvents", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, +] EVENT_CONTRACT_DATA = { "bytecode": EVENT_CONTRACT_BYTECODE, "bytecode_runtime": EVENT_CONTRACT_RUNTIME, @@ -17,7 +51,41 @@ # source: web3/_utils/contract_sources/EventContracts.sol:IndexedEventContract INDEXED_EVENT_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b506101708061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100e7565b610049565b005b807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560405160405180910390a27f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100a59190610121565b60405180910390a150565b5f5ffd5b5f819050919050565b6100c6816100b4565b81146100d0575f5ffd5b50565b5f813590506100e1816100bd565b92915050565b5f602082840312156100fc576100fb6100b0565b5b5f610109848285016100d3565b91505092915050565b61011b816100b4565b82525050565b5f6020820190506101345f830184610112565b9291505056fea2646970667358221220d6260750ec274a719dbff1334e0c10993ba5e8b99bd2807ffd773256ed41a2e164736f6c634300081b0033" # noqa: E501 INDEXED_EVENT_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b610047600480360381019061004291906100e7565b610049565b005b807ff70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb1560405160405180910390a27f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100a59190610121565b60405180910390a150565b5f5ffd5b5f819050919050565b6100c6816100b4565b81146100d0575f5ffd5b50565b5f813590506100e1816100bd565b92915050565b5f602082840312156100fc576100fb6100b0565b5b5f610109848285016100d3565b91505092915050565b61011b816100b4565b82525050565b5f6020820190506101345f830184610112565b9291505056fea2646970667358221220d6260750ec274a719dbff1334e0c10993ba5e8b99bd2807ffd773256ed41a2e164736f6c634300081b0033" # noqa: E501 -INDEXED_EVENT_CONTRACT_ABI = [{'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleArg', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': True, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleWithIndex', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '_arg0', 'type': 'uint256'}], 'name': 'logTwoEvents', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}] +INDEXED_EVENT_CONTRACT_ABI = [ + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleArg", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleWithIndex", + "type": "event", + }, + { + "inputs": [{"internalType": "uint256", "name": "_arg0", "type": "uint256"}], + "name": "logTwoEvents", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, +] INDEXED_EVENT_CONTRACT_DATA = { "bytecode": INDEXED_EVENT_CONTRACT_BYTECODE, "bytecode_runtime": INDEXED_EVENT_CONTRACT_RUNTIME, @@ -28,11 +96,43 @@ # source: web3/_utils/contract_sources/EventContracts.sol:AmbiguousEventNameContract AMBIGUOUS_EVENT_NAME_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b506102728061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b61004760048036038101906100429190610119565b610049565b005b7f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100789190610153565b60405180910390a17fe466ad4edc182e32048f6e723b179ae20d1030f298fcfa1e9ad4a759b5a63112816040516020016100b29190610153565b6040516020818303038152906040526100ca906101ae565b6040516100d79190610223565b60405180910390a150565b5f5ffd5b5f819050919050565b6100f8816100e6565b8114610102575f5ffd5b50565b5f81359050610113816100ef565b92915050565b5f6020828403121561012e5761012d6100e2565b5b5f61013b84828501610105565b91505092915050565b61014d816100e6565b82525050565b5f6020820190506101665f830184610144565b92915050565b5f81519050919050565b5f819050602082019050919050565b5f819050919050565b5f6101998251610185565b80915050919050565b5f82821b905092915050565b5f6101b88261016c565b826101c284610176565b90506101cd8161018e565b9250602082101561020d576102087fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff836020036008026101a2565b831692505b5050919050565b61021d81610185565b82525050565b5f6020820190506102365f830184610214565b9291505056fea2646970667358221220e921a9ff848699a59d4bfe4f7fe3c6ea2c735d2d7bee0857548a605a86827b3664736f6c634300081b0033" # noqa: E501 AMBIGUOUS_EVENT_NAME_CONTRACT_RUNTIME = "0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80635818fad71461002d575b5f5ffd5b61004760048036038101906100429190610119565b610049565b005b7f56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4816040516100789190610153565b60405180910390a17fe466ad4edc182e32048f6e723b179ae20d1030f298fcfa1e9ad4a759b5a63112816040516020016100b29190610153565b6040516020818303038152906040526100ca906101ae565b6040516100d79190610223565b60405180910390a150565b5f5ffd5b5f819050919050565b6100f8816100e6565b8114610102575f5ffd5b50565b5f81359050610113816100ef565b92915050565b5f6020828403121561012e5761012d6100e2565b5b5f61013b84828501610105565b91505092915050565b61014d816100e6565b82525050565b5f6020820190506101665f830184610144565b92915050565b5f81519050919050565b5f819050602082019050919050565b5f819050919050565b5f6101998251610185565b80915050919050565b5f82821b905092915050565b5f6101b88261016c565b826101c284610176565b90506101cd8161018e565b9250602082101561020d576102087fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff836020036008026101a2565b831692505b5050919050565b61021d81610185565b82525050565b5f6020820190506102365f830184610214565b9291505056fea2646970667358221220e921a9ff848699a59d4bfe4f7fe3c6ea2c735d2d7bee0857548a605a86827b3664736f6c634300081b0033" # noqa: E501 -AMBIGUOUS_EVENT_NAME_CONTRACT_ABI = [{'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'uint256', 'name': 'arg0', 'type': 'uint256'}], 'name': 'LogSingleArg', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': False, 'internalType': 'bytes32', 'name': 'arg0', 'type': 'bytes32'}], 'name': 'LogSingleArg', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '_arg0', 'type': 'uint256'}], 'name': 'logTwoEvents', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}] +AMBIGUOUS_EVENT_NAME_CONTRACT_ABI = [ + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "arg0", + "type": "uint256", + } + ], + "name": "LogSingleArg", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "bytes32", + "name": "arg0", + "type": "bytes32", + } + ], + "name": "LogSingleArg", + "type": "event", + }, + { + "inputs": [{"internalType": "uint256", "name": "_arg0", "type": "uint256"}], + "name": "logTwoEvents", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, +] AMBIGUOUS_EVENT_NAME_CONTRACT_DATA = { "bytecode": AMBIGUOUS_EVENT_NAME_CONTRACT_BYTECODE, "bytecode_runtime": AMBIGUOUS_EVENT_NAME_CONTRACT_RUNTIME, "abi": AMBIGUOUS_EVENT_NAME_CONTRACT_ABI, } - - diff --git a/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py b/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py index 51e96c88f4..f5fefd8f1a 100644 --- a/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +++ b/web3/_utils/contract_sources/contract_data/function_name_tester_contract.py @@ -6,11 +6,24 @@ # source: web3/_utils/contract_sources/FunctionNameTesterContract.sol:FunctionNameTesterContract # noqa: E501 FUNCTION_NAME_TESTER_CONTRACT_BYTECODE = "0x6080604052348015600e575f5ffd5b5060dc80601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c8063a044c987146034578063c5d7802e14604e575b5f5ffd5b603a6068565b60405160459190608f565b60405180910390f35b60546070565b604051605f9190608f565b60405180910390f35b5f6001905090565b5f5f905090565b5f8115159050919050565b6089816077565b82525050565b5f60208201905060a05f8301846082565b9291505056fea26469706673582212200a081b5e095d27bf0d18bfdb07b8618bdf1a04e1a4cf660629f12ab2b0d0bdb264736f6c634300081b0033" # noqa: E501 FUNCTION_NAME_TESTER_CONTRACT_RUNTIME = "0x6080604052348015600e575f5ffd5b50600436106030575f3560e01c8063a044c987146034578063c5d7802e14604e575b5f5ffd5b603a6068565b60405160459190608f565b60405180910390f35b60546070565b604051605f9190608f565b60405180910390f35b5f6001905090565b5f5f905090565b5f8115159050919050565b6089816077565b82525050565b5f60208201905060a05f8301846082565b9291505056fea26469706673582212200a081b5e095d27bf0d18bfdb07b8618bdf1a04e1a4cf660629f12ab2b0d0bdb264736f6c634300081b0033" # noqa: E501 -FUNCTION_NAME_TESTER_CONTRACT_ABI = [{'inputs': [], 'name': 'w3', 'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [], 'name': 'z', 'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}], 'stateMutability': 'nonpayable', 'type': 'function'}] +FUNCTION_NAME_TESTER_CONTRACT_ABI = [ + { + "inputs": [], + "name": "w3", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [], + "name": "z", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + }, +] FUNCTION_NAME_TESTER_CONTRACT_DATA = { "bytecode": FUNCTION_NAME_TESTER_CONTRACT_BYTECODE, "bytecode_runtime": FUNCTION_NAME_TESTER_CONTRACT_RUNTIME, "abi": FUNCTION_NAME_TESTER_CONTRACT_ABI, } - - diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index c0aa352043..2cfc9af2b1 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -14,7 +14,6 @@ from eth_typing import ( ABI, - ABIEvent, ChecksumAddress, ) from eth_utils import ( @@ -279,7 +278,7 @@ def __getattr__(self, event_name: str) -> "AsyncContractEvent": raise NoABIFound( "There is no ABI found for this contract.", ) - if "_events" not in self.__dict__: + if "_events" not in self.__dict__ or len(self._events) == 0: raise NoABIEventsFound( "The abi for this contract contains no event definitions. ", "Are you sure you provided the correct contract abi?", @@ -470,19 +469,6 @@ def __iter__(self) -> Iterable["AsyncContractFunction"]: for func in self._functions: yield self[abi_to_signature(func)] - def __getattribute__(self, function_name: str) -> "AsyncContractFunction": - function_identifier = function_name - - # Function names can override object attributes - if function_name in ["abi", "w3", "address"] and super().__getattribute__( - "_functions" - ): - function_identifier = _get_any_abi_signature_with_name( - function_name, super().__getattribute__("_functions") - ) - - return super().__getattribute__(function_identifier) - def __getattr__( self, function_name: str ) -> Callable[[Any, Any], "AsyncContractFunction"]: @@ -490,7 +476,7 @@ def __getattr__( raise NoABIFound( "There is no ABI found for this contract.", ) - elif "_functions" not in self.__dict__: + elif "_functions" not in self.__dict__ or len(self._functions) == 0: raise NoABIFunctionsFound( "The abi for this contract contains no function definitions. ", "Are you sure you provided the correct contract abi?", @@ -584,6 +570,16 @@ def factory( normalizers=normalizers, ), ) + + if contract.abi: + for abi in contract.abi: + abi_name = abi.get("name") + if abi_name in ["abi", "address"]: + raise Web3AttributeError( + f"Contract contains a reserved word `{abi_name}` " + f"and could not be instantiated." + ) + contract.functions = AsyncContractFunctions( contract.abi, contract.w3, decode_tuples=contract.decode_tuples ) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index cb37f14cf7..c41acfb849 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -165,7 +165,14 @@ class BaseContractEvent: def __init__(self, *argument_names: Tuple[str]) -> None: self.event_name = get_name_from_abi_element_identifier(type(self).__name__) self.abi_element_identifier = type(self).__name__ - self.abi = self._get_event_abi() + abi = self._get_event_abi() + self.name = abi_to_signature(abi) + self.abi = abi + + def __repr__(self) -> str: + if self.abi: + return f"" + return f"" @combomethod def _get_event_abi(cls) -> ABIEvent: @@ -441,22 +448,20 @@ def __init__( contract_event_type: Union[Type["ContractEvent"], Type["AsyncContractEvent"]], address: Optional[ChecksumAddress] = None, ) -> None: - # Keep a copy of each attribute to prevent variable collisions with - # contract event names - _abi = abi - _w3 = w3 - _address = address + self.abi = abi + self.w3 = w3 + self.address = address _events: Sequence[ABIEvent] = None - if _abi: - _events = filter_abi_by_type("event", _abi) + if self.abi: + _events = filter_abi_by_type("event", abi) for event in _events: abi_signature = abi_to_signature(event) event_factory = contract_event_type.factory( abi_signature, - w3=_w3, - contract_abi=_abi, - address=_address, + w3=self.w3, + contract_abi=self.abi, + address=self.address, event_name=event["name"], ) setattr(self, abi_signature, event_factory) @@ -464,10 +469,6 @@ def __init__( if _events: self._events = _events - self.abi = _abi - self.w3 = _w3 - self.address = _address - def __hasattr__(self, event_name: str) -> bool: try: return event_name in self.__dict__["_events"] @@ -704,15 +705,13 @@ def __init__( address: Optional[ChecksumAddress] = None, decode_tuples: Optional[bool] = False, ) -> None: - # Keep a copy of each attribute to prevent variable collisions with - # contract function names - _abi = abi - _w3 = w3 - _address = address + self.abi = abi + self.w3 = w3 + self.address = address _functions: Sequence[ABIFunction] = None - if _abi: - _functions = filter_abi_by_type("function", _abi) + if self.abi: + _functions = filter_abi_by_type("function", self.abi) for func in _functions: abi_signature = abi_to_signature(func) setattr( @@ -720,9 +719,9 @@ def __init__( abi_signature, contract_function_class.factory( abi_signature, - w3=_w3, - contract_abi=_abi, - address=_address, + w3=self.w3, + contract_abi=self.abi, + address=self.address, decode_tuples=decode_tuples, ), ) @@ -730,10 +729,6 @@ def __init__( if _functions: self._functions = _functions - self.abi = _abi - self.w3 = _w3 - self.address = _address - def __hasattr__(self, function_name: str) -> bool: try: return function_name in self.__dict__["_functions"] diff --git a/web3/contract/contract.py b/web3/contract/contract.py index e3a623ec5e..6644f22157 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -13,7 +13,6 @@ from eth_typing import ( ABI, - ABIEvent, ChecksumAddress, ) from eth_utils import ( @@ -256,9 +255,7 @@ def build_filter(self) -> EventFilterBuilder: @classmethod def factory(cls, class_name: str, **kwargs: Any) -> Self: - return PropertyCheckingFactory(class_name, (cls,), kwargs)( - abi=kwargs.get("abi") - ) + return PropertyCheckingFactory(class_name, (cls,), kwargs)() class ContractEvents(BaseContractEvents): @@ -272,7 +269,7 @@ def __getattr__(self, event_name: str) -> "ContractEvent": raise NoABIFound( "There is no ABI found for this contract.", ) - if "_events" not in self.__dict__: + if "_events" not in self.__dict__ or len(self._events) == 0: raise NoABIEventsFound( "The abi for this contract contains no event definitions. ", "Are you sure you provided the correct contract abi?", @@ -467,19 +464,6 @@ def __iter__(self) -> Iterable["ContractFunction"]: for func in self._functions: yield self[abi_to_signature(func)] - def __getattribute__(self, function_name: str) -> "ContractFunction": - function_identifier = function_name - - # Function names can override object attributes - if function_name in ["abi", "w3", "address"] and super().__getattribute__( - "_functions" - ): - function_identifier = _get_any_abi_signature_with_name( - function_name, super().__getattribute__("_functions") - ) - - return super().__getattribute__(function_identifier) - def __getattr__( self, function_name: str ) -> Callable[[Any, Any], "ContractFunction"]: @@ -487,7 +471,7 @@ def __getattr__( raise NoABIFound( "There is no ABI found for this contract.", ) - elif "_functions" not in self.__dict__: + elif "_functions" not in self.__dict__ or len(self._functions) == 0: raise NoABIFunctionsFound( "The abi for this contract contains no function definitions. ", "Are you sure you provided the correct contract abi?", @@ -587,6 +571,16 @@ def factory( normalizers=normalizers, ), ) + + if contract.abi: + for abi in contract.abi: + abi_name = abi.get("name") + if abi_name in ["abi", "address"]: + raise Web3AttributeError( + f"Contract contains a reserved word `{abi_name}` " + f"and could not be instantiated." + ) + contract.functions = ContractFunctions( contract.abi, contract.w3, decode_tuples=contract.decode_tuples ) From e5b1ba35bd8d4e2011a0b879d679650bb0cfe314 Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Thu, 24 Oct 2024 17:08:07 -0600 Subject: [PATCH 3/8] Fix --- web3/contract/base_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index c41acfb849..d956668278 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -169,6 +169,12 @@ def __init__(self, *argument_names: Tuple[str]) -> None: self.name = abi_to_signature(abi) self.abi = abi + if argument_names is None: + # https://github.com/python/mypy/issues/6283 + self.argument_names = tuple() # type: ignore + else: + self.argument_names = argument_names + def __repr__(self) -> str: if self.abi: return f"" From bfde77e4dbb4e653e5f37ea66e27ce6c185ab3ff Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Fri, 25 Oct 2024 11:14:41 -0600 Subject: [PATCH 4/8] Additional tests for ambiguous functions or events in a contract --- .../test_contract_ambiguous_functions.py | 159 ++++++++++++++---- web3/contract/async_contract.py | 53 +++++- web3/contract/base_contract.py | 5 +- web3/contract/contract.py | 49 +++++- 4 files changed, 214 insertions(+), 52 deletions(-) diff --git a/tests/core/contracts/test_contract_ambiguous_functions.py b/tests/core/contracts/test_contract_ambiguous_functions.py index 42c5dcc878..bbdc0632bc 100644 --- a/tests/core/contracts/test_contract_ambiguous_functions.py +++ b/tests/core/contracts/test_contract_ambiguous_functions.py @@ -1,5 +1,11 @@ import pytest +from typing import ( + cast, +) +from eth_typing import ( + ABI, +) from eth_utils.toolz import ( compose, curry, @@ -11,41 +17,63 @@ from web3.exceptions import ( Web3ValueError, ) +from web3.utils.abi import ( + get_abi_element, +) + +BLOCK_HASH_ABI = { + "constant": False, + "inputs": [{"name": "input", "type": "uint256"}], + "name": "blockHashAmphithyronVersify", + "outputs": [{"name": "", "type": "uint256"}], + "payable": False, + "stateMutability": "nonpayable", + "type": "function", +} + +IDENTITY_WITH_UINT_ABI = { + "constant": False, + "inputs": [ + {"name": "input", "type": "uint256"}, + {"name": "uselessFlag", "type": "bool"}, + ], + "name": "identity", + "outputs": [{"name": "", "type": "uint256"}], + "payable": False, + "stateMutability": "nonpayable", + "type": "function", +} + +IDENTITY_WITH_INT_ABI = { + "constant": False, + "inputs": [ + {"name": "input", "type": "int256"}, + {"name": "uselessFlag", "type": "bool"}, + ], + "name": "identity", + "outputs": [{"name": "", "type": "int256"}], + "payable": False, + "stateMutability": "nonpayable", + "type": "function", +} + +IDENTITY_WITH_BOOL_ABI = { + "constant": False, + "inputs": [ + {"name": "valid", "type": "bool"}, + ], + "name": "identity", + "outputs": [{"name": "valid", "type": "bool"}], + "payable": False, + "stateMutability": "nonpayable", + "type": "function", +} AMBIGUOUS_CONTRACT_ABI = [ - { - "constant": False, - "inputs": [{"name": "input", "type": "uint256"}], - "name": "blockHashAmphithyronVersify", - "outputs": [{"name": "", "type": "uint256"}], - "payable": False, - "stateMutability": "nonpayable", - "type": "function", - }, - { - "constant": False, - "inputs": [ - {"name": "input", "type": "uint256"}, - {"name": "uselessFlag", "type": "bool"}, - ], - "name": "identity", - "outputs": [{"name": "", "type": "uint256"}], - "payable": False, - "stateMutability": "nonpayable", - "type": "function", - }, - { - "constant": False, - "inputs": [ - {"name": "input", "type": "int256"}, - {"name": "uselessFlag", "type": "bool"}, - ], - "name": "identity", - "outputs": [{"name": "", "type": "int256"}], - "payable": False, - "stateMutability": "nonpayable", - "type": "function", - }, + BLOCK_HASH_ABI, + IDENTITY_WITH_UINT_ABI, + IDENTITY_WITH_INT_ABI, + IDENTITY_WITH_BOOL_ABI, ] @@ -75,6 +103,7 @@ def string_contract(w3, string_contract_factory, address_conversion_func): "", "", "", + "", ], ), ( @@ -93,7 +122,11 @@ def string_contract(w3, string_contract_factory, address_conversion_func): "find_functions_by_name", ("identity",), map_repr, - ["", ""], + [ + "", + "", + "", + ], ), ( "get_function_by_name", @@ -248,3 +281,61 @@ def test_diff_between_fn_and_fn_called(string_contract): assert get_value_func is not get_value_func_called assert repr(get_value_func) == "" assert repr(get_value_func_called) == "" + + +def test_get_abi_element_for_ambiguous_functions() -> None: + function_abi = get_abi_element( + cast(ABI, AMBIGUOUS_CONTRACT_ABI), + "identity", + *[ + 2**256 - 1, # uint256 maximum + False, + ], + ) + + assert function_abi == IDENTITY_WITH_UINT_ABI + + function_abi = get_abi_element( + cast(ABI, AMBIGUOUS_CONTRACT_ABI), + "identity", + *[ + -1, + True, + ], + ) + + assert function_abi == IDENTITY_WITH_INT_ABI + + function_abi = get_abi_element( + cast(ABI, AMBIGUOUS_CONTRACT_ABI), + "identity", + *[ + False, + ], + ) + + assert function_abi == IDENTITY_WITH_BOOL_ABI + + +def test_get_abi_element_for_ambiguous_function_arguments(): + function_abi = get_abi_element( + cast(ABI, AMBIGUOUS_CONTRACT_ABI), + "identity(int256,bool)", + *[ + 1, + False, + ], + ) + + assert function_abi == IDENTITY_WITH_INT_ABI + + function_abi = get_abi_element( + cast(ABI, AMBIGUOUS_CONTRACT_ABI), + "identity(uint256,bool)", + *[ + 1, + False, + ], + ) + + assert function_abi == IDENTITY_WITH_UINT_ABI diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 2cfc9af2b1..4b83eefb3d 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -108,6 +108,7 @@ ) from web3.utils.abi import ( _get_any_abi_signature_with_name, + filter_abi_by_type, get_abi_element, ) @@ -121,7 +122,26 @@ class AsyncContractEvent(BaseContractEvent): w3: "AsyncWeb3" def __call__(self, *args: Any, **kwargs: Any) -> "AsyncContractEvent": - return copy_contract_event(self, *args, **kwargs) + event_abi = get_abi_element( + filter_abi_by_type("event", self.contract_abi), + self.name, + *args, + abi_codec=self.w3.codec, + **kwargs, + ) + argument_types = get_abi_input_types(event_abi) + event_signature = str( + get_abi_element_signature(self.abi_element_identifier, argument_types) + ) + contract_event = AsyncContractEvent.factory( + event_signature, + w3=self.w3, + contract_abi=self.contract_abi, + address=self.address, + abi_element_identifier=event_signature, + ) + + return copy_contract_event(contract_event, *args, **kwargs) @combomethod async def get_logs( @@ -255,9 +275,7 @@ def build_filter(self) -> AsyncEventFilterBuilder: @classmethod def factory(cls, class_name: str, **kwargs: Any) -> Self: - return PropertyCheckingFactory(class_name, (cls,), kwargs)( - abi=kwargs.get("abi") - ) + return PropertyCheckingFactory(class_name, (cls,), kwargs)() class AsyncContractEvents(BaseContractEvents): @@ -306,11 +324,30 @@ class AsyncContractFunction(BaseContractFunction): w3: "AsyncWeb3" def __call__(self, *args: Any, **kwargs: Any) -> "AsyncContractFunction": - return copy_contract_function(self, *args, **kwargs) + function_abi = get_abi_element( + filter_abi_by_type("function", self.contract_abi), + self.name, + *args, + abi_codec=self.w3.codec, + **kwargs, + ) + argument_types = get_abi_input_types(function_abi) + function_signature = str( + get_abi_element_signature(self.abi_element_identifier, argument_types) + ) + contract_function = AsyncContractFunction.factory( + function_signature, + w3=self.w3, + contract_abi=self.contract_abi, + address=self.address, + abi_element_identifier=function_signature, + ) + + return copy_contract_function(contract_function, *args, **kwargs) @classmethod def factory(cls, class_name: str, **kwargs: Any) -> Self: - return PropertyCheckingFactory(class_name, (cls,), kwargs)(kwargs.get("abi")) + return PropertyCheckingFactory(class_name, (cls,), kwargs)() async def call( self, @@ -469,9 +506,7 @@ def __iter__(self) -> Iterable["AsyncContractFunction"]: for func in self._functions: yield self[abi_to_signature(func)] - def __getattr__( - self, function_name: str - ) -> Callable[[Any, Any], "AsyncContractFunction"]: + def __getattr__(self, function_name: str) -> "AsyncContractFunction": if super().__getattribute__("abi") is None: raise NoABIFound( "There is no ABI found for this contract.", diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index d956668278..460281fe8b 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -516,6 +516,7 @@ def __init__(self, abi: Optional[ABIFunction] = None) -> None: self.abi_element_identifier, ), ) + self.name = abi_to_signature(self.abi) @combomethod def _get_abi(cls) -> ABIFunction: @@ -691,9 +692,7 @@ def __repr__(self) -> str: def factory( cls, class_name: str, **kwargs: Any ) -> Union["ContractFunction", "AsyncContractFunction"]: - return PropertyCheckingFactory(class_name, (cls,), kwargs)( - abi=kwargs.get("abi") - ) + return PropertyCheckingFactory(class_name, (cls,), kwargs)() class BaseContractFunctions: diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 6644f22157..0717d79e2b 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -105,6 +105,7 @@ ) from web3.utils.abi import ( _get_any_abi_signature_with_name, + filter_abi_by_type, get_abi_element, ) @@ -118,7 +119,26 @@ class ContractEvent(BaseContractEvent): w3: "Web3" def __call__(self, *args: Any, **kwargs: Any) -> "ContractEvent": - return copy_contract_event(self, *args, **kwargs) + event_abi = get_abi_element( + filter_abi_by_type("event", self.contract_abi), + self.name, + *args, + abi_codec=self.w3.codec, + **kwargs, + ) + argument_types = get_abi_input_types(event_abi) + event_signature = str( + get_abi_element_signature(self.abi_element_identifier, argument_types) + ) + contract_event = ContractEvent.factory( + event_signature, + w3=self.w3, + contract_abi=self.contract_abi, + address=self.address, + abi_element_identifier=event_signature, + ) + + return copy_contract_event(contract_event, *args, **kwargs) @combomethod def get_logs( @@ -301,11 +321,30 @@ class ContractFunction(BaseContractFunction): w3: "Web3" def __call__(self, *args: Any, **kwargs: Any) -> "ContractFunction": - return copy_contract_function(self, *args, **kwargs) + function_abi = get_abi_element( + filter_abi_by_type("function", self.contract_abi), + self.name, + *args, + abi_codec=self.w3.codec, + **kwargs, + ) + argument_types = get_abi_input_types(function_abi) + function_signature = str( + get_abi_element_signature(self.abi_element_identifier, argument_types) + ) + contract_function = ContractFunction.factory( + function_signature, + w3=self.w3, + contract_abi=self.contract_abi, + address=self.address, + abi_element_identifier=function_signature, + ) + + return copy_contract_function(contract_function, *args, **kwargs) @classmethod def factory(cls, class_name: str, **kwargs: Any) -> Self: - return PropertyCheckingFactory(class_name, (cls,), kwargs)(kwargs.get("abi")) + return PropertyCheckingFactory(class_name, (cls,), kwargs)() def call( self, @@ -464,9 +503,7 @@ def __iter__(self) -> Iterable["ContractFunction"]: for func in self._functions: yield self[abi_to_signature(func)] - def __getattr__( - self, function_name: str - ) -> Callable[[Any, Any], "ContractFunction"]: + def __getattr__(self, function_name: str) -> "ContractFunction": if super().__getattribute__("abi") is None: raise NoABIFound( "There is no ABI found for this contract.", From e3d21df836b0fb37d086ccc54ee95c449854a464 Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Fri, 25 Oct 2024 16:20:46 -0600 Subject: [PATCH 5/8] Fix calls to get_abi_element for functions with and without arguments --- tests/core/utilities/test_abi.py | 10 ++++++++++ web3/_utils/abi.py | 4 +--- web3/contract/async_contract.py | 20 +++++++++++++++++--- web3/contract/base_contract.py | 13 ++++++++++++- web3/contract/contract.py | 20 +++++++++++++++++--- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py index 6e8d39024c..4ccc585aa2 100644 --- a/tests/core/utilities/test_abi.py +++ b/tests/core/utilities/test_abi.py @@ -176,6 +176,9 @@ ] ABI_CONSTRUCTOR = ABIConstructor({"type": "constructor"}) +ABI_CONSTRUCTOR_WITH_ARGS = ABIConstructor( + {"type": "constructor", "inputs": [{"name": "x", "type": "address"}]} +) ABI_FALLBACK = ABIFallback({"type": "fallback"}) @@ -518,6 +521,13 @@ def test_get_abi_element_info_raises_mismatched_abi(contract_abi: ABI) -> None: {}, ABI_CONSTRUCTOR, ), + ( + [ABI_CONSTRUCTOR_WITH_ARGS], + "constructor", + ["0x0000000000000000000000000000000000000000"], + {}, + ABI_CONSTRUCTOR_WITH_ARGS, + ), ( CONTRACT_ABI_AMBIGUOUS_EVENT, "LogSingleArg()", diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 16b442ef4c..6a2f280072 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -178,8 +178,6 @@ def get_name_from_abi_element_identifier( return "fallback" elif abi_element_identifier in ["receive", ReceiveFn]: return "receive" - elif abi_element_identifier == "constructor": - return "constructor" elif is_text(abi_element_identifier): return str(abi_element_identifier).split("(")[0] else: @@ -193,7 +191,7 @@ def get_abi_element_signature( element_name = get_name_from_abi_element_identifier(abi_element_identifier) argument_types = ",".join(abi_element_argument_types or []) - if element_name in ["fallback", "receive", "constructor"]: + if element_name in ["fallback", "receive"]: return element_name return f"{element_name}({argument_types})" diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 4b83eefb3d..80c4f131a3 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -34,6 +34,7 @@ from web3._utils.abi import ( fallback_func_abi_exists, + filter_by_types, get_abi_element_signature, get_name_from_abi_element_identifier, receive_func_abi_exists, @@ -324,14 +325,27 @@ class AsyncContractFunction(BaseContractFunction): w3: "AsyncWeb3" def __call__(self, *args: Any, **kwargs: Any) -> "AsyncContractFunction": + element_name = self.abi_element_identifier + if element_name in ["fallback", "receive"] or len(args) + len(kwargs): + # Use only the name if a fallback, receive function + # or when args/kwargs are present to find the proper element + element_name = self.fn_name + function_abi = get_abi_element( - filter_abi_by_type("function", self.contract_abi), - self.name, + filter_by_types( + ["function", "constructor", "fallback", "receive"], + self.contract_abi, + ), + element_name, *args, abi_codec=self.w3.codec, **kwargs, ) - argument_types = get_abi_input_types(function_abi) + + argument_types = None + if function_abi["type"] not in ["fallback", "receive"]: + argument_types = get_abi_input_types(function_abi) + function_signature = str( get_abi_element_signature(self.abi_element_identifier, argument_types) ) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 460281fe8b..146cf8dc95 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -155,6 +155,7 @@ class BaseContractEvent: address: ChecksumAddress = None event_name: str = None + abi_element_identifier: ABIElementIdentifier = None w3: Union["Web3", "AsyncWeb3"] = None contract_abi: ABI = None abi: ABIEvent = None @@ -491,6 +492,8 @@ class BaseContractFunction: """ address: ChecksumAddress = None + fn_name: str = None + name: str = None abi_element_identifier: ABIElementIdentifier = None w3: Union["Web3", "AsyncWeb3"] = None contract_abi: ABI = None @@ -544,7 +547,15 @@ def _get_abi(cls) -> ABIFunction: def _set_function_info(self) -> None: self.selector = encode_hex(b"") - if self.abi_element_identifier in [FallbackFn, ReceiveFn]: + if self.abi_element_identifier in [ + "fallback", + "receive", + FallbackFn, + ReceiveFn, + ]: + self.abi = self._get_abi() + + self.selector = encode_hex(function_abi_to_4byte_selector(self.abi)) self.arguments = None elif is_text(self.abi_element_identifier): self.abi = self._get_abi() diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 0717d79e2b..264349871f 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -31,6 +31,7 @@ from web3._utils.abi import ( fallback_func_abi_exists, + filter_by_types, get_abi_element_signature, get_name_from_abi_element_identifier, receive_func_abi_exists, @@ -321,14 +322,27 @@ class ContractFunction(BaseContractFunction): w3: "Web3" def __call__(self, *args: Any, **kwargs: Any) -> "ContractFunction": + element_name = self.abi_element_identifier + if element_name in ["fallback", "receive"] or len(args) + len(kwargs): + # Use only the name if a fallback, receive function + # or when args/kwargs are present to find the proper element + element_name = self.fn_name + function_abi = get_abi_element( - filter_abi_by_type("function", self.contract_abi), - self.name, + filter_by_types( + ["function", "constructor", "fallback", "receive"], + self.contract_abi, + ), + element_name, *args, abi_codec=self.w3.codec, **kwargs, ) - argument_types = get_abi_input_types(function_abi) + + argument_types = None + if function_abi["type"] not in ["fallback", "receive"]: + argument_types = get_abi_input_types(function_abi) + function_signature = str( get_abi_element_signature(self.abi_element_identifier, argument_types) ) From 0b46954e7578ead8ece92a53348902040355dc1c Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Mon, 28 Oct 2024 10:13:25 -0600 Subject: [PATCH 6/8] Pass decode tuples into factory call --- web3/contract/async_contract.py | 1 + web3/contract/base_contract.py | 2 +- web3/contract/contract.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 80c4f131a3..b661f41e72 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -355,6 +355,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> "AsyncContractFunction": contract_abi=self.contract_abi, address=self.address, abi_element_identifier=function_signature, + decode_tuples=self.decode_tuples, ) return copy_contract_function(contract_function, *args, **kwargs) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 146cf8dc95..20146b8747 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -500,7 +500,7 @@ class BaseContractFunction: abi: ABIFunction = None transaction: TxParams = None arguments: Tuple[Any, ...] = None - decode_tuples: Optional[bool] = False + decode_tuples: Optional[bool] = None args: Any = None kwargs: Any = None diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 264349871f..24cd30e57d 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -352,6 +352,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> "ContractFunction": contract_abi=self.contract_abi, address=self.address, abi_element_identifier=function_signature, + decode_tuples=self.decode_tuples, ) return copy_contract_function(contract_function, *args, **kwargs) From 9672113e74f28ab98a5f0b9842256d7acc7b9982 Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Mon, 28 Oct 2024 11:18:46 -0600 Subject: [PATCH 7/8] Split tests and additional feedback --- .../test_contract_class_construction.py | 41 +++++++++++++------ tests/core/utilities/test_abi.py | 10 +++-- web3/utils/abi.py | 11 +++-- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/tests/core/contracts/test_contract_class_construction.py b/tests/core/contracts/test_contract_class_construction.py index af8c992d27..6a2e3629bd 100644 --- a/tests/core/contracts/test_contract_class_construction.py +++ b/tests/core/contracts/test_contract_class_construction.py @@ -82,8 +82,6 @@ def test_error_to_call_non_existent_fallback( @pytest.mark.parametrize( "abi,namespace,expected_exception", ( - (None, "functions", NoABIFound), - (None, "events", NoABIFound), ([{"type": "event", "name": "AnEvent"}], "functions", NoABIFunctionsFound), ([{"type": "function", "name": "aFunction"}], "events", NoABIEventsFound), ([{"type": "function", "name": "aFunction"}], "functions", ABIFunctionNotFound), @@ -93,10 +91,7 @@ def test_error_to_call_non_existent_fallback( def test_appropriate_exceptions_based_on_namespaces( w3, abi, namespace, expected_exception ): - if abi is None: - contract = w3.eth.contract() - else: - contract = w3.eth.contract(abi=abi) + contract = w3.eth.contract(abi=abi) namespace_instance = getattr(contract, namespace) @@ -104,11 +99,22 @@ def test_appropriate_exceptions_based_on_namespaces( namespace_instance.doesNotExist() +@pytest.mark.parametrize( + "namespace", + ("functions", "events"), +) +def test_appropriate_exceptions_based_on_namespaces_no_abi(w3, namespace): + contract = w3.eth.contract() + + namespace_instance = getattr(contract, namespace) + + with pytest.raises(NoABIFound): + namespace_instance.doesNotExist() + + @pytest.mark.parametrize( "abi,namespace,expected_exception", ( - (None, "functions", NoABIFound), - (None, "events", NoABIFound), ([{"type": "event", "name": "AnEvent"}], "functions", NoABIFunctionsFound), ([{"type": "function", "name": "aFunction"}], "events", NoABIEventsFound), ([{"type": "function", "name": "aFunction"}], "functions", ABIFunctionNotFound), @@ -118,12 +124,23 @@ def test_appropriate_exceptions_based_on_namespaces( def test_async_appropriate_exceptions_based_on_namespaces( async_w3, abi, namespace, expected_exception ): - if abi is None: - contract = async_w3.eth.contract() - else: - contract = async_w3.eth.contract(abi=abi) + contract = async_w3.eth.contract(abi=abi) namespace_instance = getattr(contract, namespace) with pytest.raises(expected_exception): namespace_instance.doesNotExist() + + +@pytest.mark.parametrize( + "namespace", + ("functions", "events"), +) +def test_async_appropriate_exceptions_based_on_namespaces_no_abi(async_w3, namespace): + # Initialize without ABI + contract = async_w3.eth.contract() + + namespace_instance = getattr(contract, namespace) + + with pytest.raises(NoABIFound): + namespace_instance.doesNotExist() diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py index 4ccc585aa2..cbb8c4de81 100644 --- a/tests/core/utilities/test_abi.py +++ b/tests/core/utilities/test_abi.py @@ -864,8 +864,9 @@ def test_get_event_abi_raises_on_error( "type": "function", } ] - with pytest.raises(error_type, match=expected_value): - get_event_abi(contract_abi, name, args) + with pytest.warns(DeprecationWarning): + with pytest.raises(error_type, match=expected_value): + get_event_abi(contract_abi, name, args) def test_get_event_abi_raises_if_multiple_found() -> None: @@ -883,5 +884,6 @@ def test_get_event_abi_raises_if_multiple_found() -> None: "type": "event", }, ] - with pytest.raises(ValueError, match="Multiple events found"): - get_event_abi(contract_ambiguous_event, "LogSingleArg", ["arg0"]) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match="Multiple events found"): + get_event_abi(contract_ambiguous_event, "LogSingleArg", ["arg0"]) diff --git a/web3/utils/abi.py b/web3/utils/abi.py index 8ac924c8d4..ef1bd4c338 100644 --- a/web3/utils/abi.py +++ b/web3/utils/abi.py @@ -172,10 +172,9 @@ def _get_any_abi_signature_with_name(element_name: str, contract_abi: ABI) -> st Find an ABI identifier signature by element name. A signature identifier is returned, "name(arg1Type,arg2Type,...)". - This function forces a result to be returned even if multiple are found. Depending - on the ABIs found, the signature may be returned with or without arguments types. - The signature without arguments is returned when there is a matching ABI with no - arguments. + This function forces one result to be returned even if multiple are found. + If multiple ABIs are found and all contain arguments, the first result is returned. + Otherwise when one of the ABIs has zero arguments, that signature is returned. """ try: # search for function abis with the same name @@ -552,6 +551,10 @@ def get_abi_element( Return the interface for an ``ABIElement`` from the ``abi`` that matches the provided identifier and arguments. + ``abi`` may be a list of all ABI elements in a contract or a subset of elements. + Passing only functions or events can be useful when names are not deterministic. + For example, if names overlap between functions and events. + The ``ABIElementIdentifier`` value may be a function name, signature, or a ``FallbackFn`` or ``ReceiveFn``. When named arguments (``args``) and/or keyword args (``kwargs``) are provided, they are included in the search filters. From 747ceab1d029b2d79e21042dfb0fc8c4d100d81b Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Tue, 29 Oct 2024 09:26:40 -0600 Subject: [PATCH 8/8] Test for issue #3482 --- .../test_contract_ambiguous_functions.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/core/contracts/test_contract_ambiguous_functions.py b/tests/core/contracts/test_contract_ambiguous_functions.py index bbdc0632bc..7cf3f2262c 100644 --- a/tests/core/contracts/test_contract_ambiguous_functions.py +++ b/tests/core/contracts/test_contract_ambiguous_functions.py @@ -266,6 +266,32 @@ def test_functions_error_messages(w3, method, args, expected_message, expected_e getattr(contract, method)(*args) +def test_ambiguous_functions_abi_element_identifier(w3): + abi = [ + { + "name": "isValidSignature", + "type": "function", + "inputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "bytes", "name": "id", "type": "bytes"}, + ], + }, + { + "name": "isValidSignature", + "type": "function", + "inputs": [ + {"internalType": "bytes", "name": "id", "type": "bytes"}, + {"internalType": "bytes", "name": "id", "type": "bytes"}, + ], + }, + ] + contract = w3.eth.contract(abi=abi) + fn_bytes = contract.get_function_by_signature("isValidSignature(bytes,bytes)") + assert fn_bytes.abi_element_identifier == "isValidSignature(bytes,bytes)" + fn_bytes32 = contract.get_function_by_signature("isValidSignature(bytes32,bytes)") + assert fn_bytes32.abi_element_identifier == "isValidSignature(bytes32,bytes)" + + def test_contract_function_methods(string_contract): set_value_func = string_contract.get_function_by_signature("setValue(string)") get_value_func = string_contract.get_function_by_signature("getValue()")