Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Descriptive Event ABI Error Messages #3453

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/web3.contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ Taking the following contract code as an example:
Traceback (most recent call last):
...
web3.exceptions.MismatchedABI:
Could not identify the intended function with name
Could not identify the intended ABI with name
>>> # check value is still b'aa'
>>> arrays_contract.functions.getBytes2Value().call()
[b'aa']
Expand All @@ -651,7 +651,7 @@ Taking the following contract code as an example:
Traceback (most recent call last):
...
web3.exceptions.MismatchedABI:
Could not identify the intended function with name
Could not identify the intended ABI with name

.. _contract-functions:

Expand Down
1 change: 1 addition & 0 deletions newsfragments/964.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise ``MismatchedABI`` errors in ``get_event_abi`` when the arguments do not match the ``ABI``.
60 changes: 28 additions & 32 deletions tests/core/contracts/test_contract_call_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,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="Could not identify the intended ABI with name `setByteValue`",
):
transact(
contract=arrays_contract,
Expand Down Expand Up @@ -594,19 +594,15 @@ def test_returns_data_from_specified_block(w3, math_contract):


message_regex = (
r"\nCould not identify the intended function with name `.*`, positional arguments "
r"\nCould not identify the intended ABI 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."
r"\nFound .* ABI\(s\) with the name `.*`: .*"
)
diagnosis_arg_regex = r"\nNo ABIs match the given number of arguments."
diagnosis_encoding_regex = r"\nNo ABIs match the encoded argument types."
diagnosis_ambiguous_encoding = (
r"\nAmbiguous argument encoding. "
r"Provided arguments can be encoded to multiple functions matching this call."
r"Provided arguments can be encoded to multiple ABIs matching this call."
)


Expand Down Expand Up @@ -761,29 +757,29 @@ 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"), diagnosis_encoding_regex),
("reflect_short_u", Decimal("-.1"), diagnosis_encoding_regex),
# too many digits for *x1, too large for 256x80
("reflect", Decimal("0.01"), "no matching argument types"),
("reflect", Decimal("0.01"), diagnosis_encoding_regex),
# too many digits
("reflect_short_u", Decimal("0.01"), "no matching argument types"),
("reflect_short_u", Decimal("0.01"), diagnosis_encoding_regex),
(
"reflect_short_u",
Decimal(f"1e-{DEFAULT_DECIMALS + 1}"),
"no matching argument types",
diagnosis_encoding_regex,
),
(
"reflect_short_u",
Decimal("25.4" + "9" * DEFAULT_DECIMALS),
"no matching argument types",
diagnosis_encoding_regex,
),
("reflect", Decimal(1) / 10**81, "no matching argument types"),
("reflect", Decimal(1) / 10**81, diagnosis_encoding_regex),
# floats not accepted, for floating point error concerns
("reflect_short_u", 0.1, "no matching argument types"),
("reflect_short_u", 0.1, diagnosis_encoding_regex),
# ambiguous
("reflect", Decimal("12.7"), "Ambiguous argument encoding"),
("reflect", Decimal(0), "Ambiguous argument encoding"),
("reflect", 0, "Ambiguous argument encoding"),
("reflect", Decimal("12.7"), diagnosis_ambiguous_encoding),
("reflect", Decimal(0), diagnosis_ambiguous_encoding),
("reflect", 0, diagnosis_ambiguous_encoding),
),
)
def test_invalid_fixed_value_reflections(
Expand Down Expand Up @@ -1891,29 +1887,29 @@ async def test_async_reflect_fixed_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"), diagnosis_encoding_regex),
("reflect_short_u", Decimal("-.1"), diagnosis_encoding_regex),
# too many digits for *x1, too large for 256x80
("reflect", Decimal("0.01"), "no matching argument types"),
("reflect", Decimal("0.01"), diagnosis_encoding_regex),
# too many digits
("reflect_short_u", Decimal("0.01"), "no matching argument types"),
("reflect_short_u", Decimal("0.01"), diagnosis_encoding_regex),
(
"reflect_short_u",
Decimal(f"1e-{DEFAULT_DECIMALS + 1}"),
"no matching argument types",
diagnosis_encoding_regex,
),
(
"reflect_short_u",
Decimal("25.4" + "9" * DEFAULT_DECIMALS),
"no matching argument types",
diagnosis_encoding_regex,
),
("reflect", Decimal(1) / 10**81, "no matching argument types"),
("reflect", Decimal(1) / 10**81, diagnosis_encoding_regex),
# floats not accepted, for floating point error concerns
("reflect_short_u", 0.1, "no matching argument types"),
("reflect_short_u", 0.1, diagnosis_encoding_regex),
# ambiguous
("reflect", Decimal("12.7"), "Ambiguous argument encoding"),
("reflect", Decimal(0), "Ambiguous argument encoding"),
("reflect", 0, "Ambiguous argument encoding"),
("reflect", Decimal("12.7"), diagnosis_ambiguous_encoding),
("reflect", Decimal(0), diagnosis_ambiguous_encoding),
("reflect", 0, diagnosis_ambiguous_encoding),
),
)
async def test_async_invalid_fixed_value_reflections(
Expand Down
17 changes: 13 additions & 4 deletions tests/core/utilities/test_abi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import re
from typing import (
Any,
Callable,
Expand Down Expand Up @@ -385,7 +386,7 @@ 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="Could not identify the intended ABI"):
args: Sequence[Any] = [1]
get_abi_element_info(contract_abi, "foo", *args, **{})

Expand Down Expand Up @@ -516,7 +517,7 @@ def test_get_abi_element(
[],
{},
MismatchedABI,
"Function invocation failed due to improper number of arguments.",
"No ABIs match the given number of arguments.",
),
),
)
Expand Down Expand Up @@ -650,6 +651,12 @@ def test_get_event_abi(event_name: str, input_args: Sequence[ABIComponent]) -> N
assert get_event_abi(contract_abi, event_name, input_names) == expected_event_abi


def test_get_event_abi_raises_mismatched_abi(contract_abi: ABI) -> None:
with pytest.raises(MismatchedABI, match="Could not identify the intended ABI"):
args: Sequence[Any] = [1]
get_event_abi(contract_abi, "foo", *args, **{})


@pytest.mark.parametrize(
"name,args,error_type,expected_value",
(
Expand All @@ -659,7 +666,7 @@ def test_get_event_abi(event_name: str, input_args: Sequence[ABIComponent]) -> N
Web3ValidationError,
"event_name is required in order to match an event ABI.",
),
("foo", None, Web3ValueError, "No matching events found"),
("foo", None, MismatchedABI, "No ABIs match the given number of arguments."),
),
)
def test_get_event_abi_raises_on_error(
Expand Down Expand Up @@ -697,5 +704,7 @@ def test_get_event_abi_raises_if_multiple_found() -> None:
"type": "event",
},
]
with pytest.raises(ValueError, match="Multiple events found"):
with pytest.raises(
MismatchedABI, match=re.escape("Found 2 ABI(s) with the name `LogSingleArg`")
):
get_event_abi(contract_ambiguous_event, "LogSingleArg", ["arg0"])
75 changes: 43 additions & 32 deletions web3/utils/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
MismatchedABI,
Web3TypeError,
Web3ValidationError,
Web3ValueError,
)
from web3.types import (
ABIElementIdentifier,
Expand Down Expand Up @@ -160,42 +159,50 @@ def _get_fallback_function_abi(contract_abi: ABI) -> ABIFallback:

def _mismatched_abi_error_diagnosis(
abi_element_identifier: ABIElementIdentifier,
matching_function_signatures: Sequence[str],
arg_count_matches: int,
encoding_matches: int,
matching_element_signatures: Sequence[str],
abi_matches: int,
*args: Optional[Sequence[Any]],
encodable_abi_matches: int = None,
**kwargs: Optional[Dict[str, Any]],
) -> str:
"""
Raise a ``MismatchedABI`` when a function ABI lookup results in an error.
Raise a ``MismatchedABI`` when an ABI lookup results in an error.

The number of `num_matches` and `encoding_matches` are used to determine the
specific error diagnosis. The `num_matches` is the number of functions with
the same name and the same number of arguments. The `encoding_matches` is the number
of functions with the same name and the same number of arguments that can be
encoded with the provided arguments and keyword arguments.

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."
)
if abi_matches == 0:
diagnosis += "No ABIs match the given number of arguments."

if encodable_abi_matches is not None:
if encodable_abi_matches == 0:
diagnosis += "No ABIs match the encoded argument types."
elif encodable_abi_matches > 1:
diagnosis += (
"Ambiguous argument encoding. "
"Provided arguments can be encoded to multiple ABIs "
"matching this call."
)

collapsed_args = _extract_argument_types(*args)
collapsed_kwargs = dict(
{(k, _extract_argument_types([v])) for k, v in kwargs.items()}
)

return (
f"\nCould not identify the intended function with name "
f"\nCould not identify the intended ABI 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}"
f"\nFound {len(matching_element_signatures)} ABI(s) with the name "
f"`{abi_element_identifier}`: {matching_element_signatures}{diagnosis}"
)


Expand Down Expand Up @@ -239,7 +246,7 @@ def get_abi_element_info(
**kwargs: Optional[Dict[str, Any]],
) -> ABIElementInfo:
"""
Information about the function ABI, selector and input arguments.
Information about the element ABI, selector and input arguments.

Returns the ABI which matches the provided identifier, named arguments (``args``)
and keyword args (``kwargs``).
Expand All @@ -248,7 +255,7 @@ def get_abi_element_info(
:type abi: `ABI`
:param abi_element_identifier: Find an element ABI with matching identifier.
:type abi_element_identifier: `ABIElementIdentifier`
:param args: Find a function ABI with matching args.
:param args: Find an element ABI with matching args.
:type args: `Optional[Sequence[Any]]`
:param abi_codec: Codec used for encoding and decoding. Default with \
`strict_bytes_type_checking` enabled.
Expand Down Expand Up @@ -391,16 +398,12 @@ def get_abi_element(
)

if len(elements_with_encodable_args) != 1:
matching_function_signatures = [
abi_to_signature(func) for func in filtered_abis_by_name
]

error_diagnosis = _mismatched_abi_error_diagnosis(
abi_element_identifier,
matching_function_signatures,
[abi_to_signature(func) for func in filtered_abis_by_name],
len(filtered_abis_by_arg_count),
len(elements_with_encodable_args),
*args,
encodable_abi_matches=len(elements_with_encodable_args),
**kwargs,
)

Expand Down Expand Up @@ -480,6 +483,10 @@ def get_event_abi(
"""
Find the event interface with the given name and/or arguments.

Unlike the `get_abi_element` function, which returns the function ABI that matches
the provided identifier and arguments, this function will return the event ABI which
matches the provided event name and argument names.

:param abi: Contract ABI.
:type abi: `ABI`
:param event_name: Find an event abi with matching event name.
Expand Down Expand Up @@ -516,12 +523,16 @@ def get_event_abi(

event_abi_candidates = cast(Sequence[ABIEvent], pipe(abi, *filters))

if len(event_abi_candidates) == 1:
return event_abi_candidates[0]
elif len(event_abi_candidates) == 0:
raise Web3ValueError("No matching events found")
else:
raise Web3ValueError("Multiple events found")
if len(event_abi_candidates) != 1:
raise MismatchedABI(
_mismatched_abi_error_diagnosis(
event_name,
[abi_to_signature(event_abi) for event_abi in event_abi_candidates],
len(event_abi_candidates),
)
)

return event_abi_candidates[0]


def get_event_log_topics(
Expand Down