Skip to content

Commit 0f8a698

Browse files
committed
Better messaging for wrong tuple arguments to contract functions
- collapse tuple types into their nested types for the recognized function signature(s) - collapse user argument types to better compare against the recognized function signature(s)
1 parent fe49585 commit 0f8a698

File tree

3 files changed

+91
-9
lines changed

3 files changed

+91
-9
lines changed

tests/core/contracts/test_contract_call_interface.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -620,9 +620,8 @@ def test_returns_data_from_specified_block(w3, math_contract):
620620

621621

622622
message_regex = (
623-
r"\nCould not identify the intended function with name `.*`, "
624-
r"positional argument\(s\) of type `.*` and "
625-
r"keyword argument\(s\) of type `.*`."
623+
r"\nCould not identify the intended function with name `.*`, positional arguments "
624+
r"with type\(s\) `.*` and keyword arguments with type\(s\) `.*`."
626625
r"\nFound .* function\(s\) with the name `.*`: .*"
627626
)
628627
diagnosis_arg_regex = (
@@ -672,6 +671,52 @@ def test_function_multiple_error_diagnoses(w3, arg1, arg2, diagnosis):
672671
Contract.functions.a(arg1).call()
673672

674673

674+
@pytest.mark.parametrize(
675+
"address", (
676+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # checksummed
677+
b'\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee', # noqa: E501
678+
)
679+
)
680+
def test_function_wrong_args_for_tuple_collapses_args_in_message(
681+
address, tuple_contract,
682+
):
683+
with pytest.raises(ValidationError) as e:
684+
tuple_contract.functions.method(
685+
(1, [2, 3], [(4, [True, [False]], [address])])
686+
).call()
687+
688+
# assert the user arguments are formatted as expected:
689+
# (int,(int,int),((int,(bool,(bool)),(address))))
690+
e.match("\\(int,\\(int,int\\),\\(\\(int,\\(bool,\\(bool\\)\\),\\(address\\)\\)\\)\\)") # noqa: E501
691+
692+
# assert the found method signature is formatted as expected:
693+
# ['method((uint256,uint256[],(int256,bool[2],address[])[]))']
694+
e.match("\\['method\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)'\\]") # noqa: E501
695+
696+
697+
@pytest.mark.parametrize(
698+
"address", (
699+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # checksummed
700+
b'\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee', # noqa: E501
701+
)
702+
)
703+
def test_function_wrong_args_for_tuple_collapses_kwargs_in_message(
704+
address, tuple_contract
705+
):
706+
with pytest.raises(ValidationError) as e:
707+
tuple_contract.functions.method(
708+
a=(1, [2, 3], [(4, [True, [False]], [address])]) # noqa: E501
709+
).call()
710+
711+
# assert the user keyword arguments are formatted as expected:
712+
# {'a': '(int,(int,int),((int,(bool,(bool)),(address))))'}
713+
e.match("{'a': '\\(int,\\(int,int\\),\\(\\(int,\\(bool,\\(bool\\)\\),\\(address\\)\\)\\)\\)'}") # noqa: E501
714+
715+
# assert the found method signature is formatted as expected:
716+
# ['method((uint256,uint256[],(int256,bool[2],address[])[]))']
717+
e.match("\\['method\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)'\\]") # noqa: E501
718+
719+
675720
def test_function_no_abi(w3):
676721
contract = w3.eth.contract()
677722
with pytest.raises(NoABIFound):

web3/_utils/abi.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,8 @@ def abi_to_signature(abi: Union[ABIFunction, ABIEvent]) -> str:
724724
function_signature = "{fn_name}({fn_input_types})".format(
725725
fn_name=abi["name"],
726726
fn_input_types=",".join(
727-
[arg["type"] for arg in normalize_event_input_types(abi.get("inputs", []))]
727+
collapse_if_tuple(dict(arg))
728+
for arg in normalize_event_input_types(abi.get("inputs", []))
728729
),
729730
)
730731
return function_signature

web3/_utils/contracts.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
add_0x_prefix,
2222
encode_hex,
2323
function_abi_to_4byte_selector,
24+
is_binary_address,
25+
is_checksum_address,
26+
is_list_like,
2427
is_text,
2528
)
2629
from eth_utils.toolz import (
2730
pipe,
28-
valmap,
2931
)
3032
from hexbytes import (
3133
HexBytes,
@@ -73,6 +75,28 @@
7375
from web3 import Web3 # noqa: F401
7476

7577

78+
def extract_argument_types(*args: Sequence[Any]) -> str:
79+
"""
80+
Takes a list of arguments and returns a string representation of the argument types,
81+
appropriately collapsing `tuple` types into the respective nested types.
82+
"""
83+
collapsed_args = []
84+
85+
for arg in args:
86+
if is_list_like(arg):
87+
collapsed_nested = []
88+
for nested in arg:
89+
if is_list_like(nested):
90+
collapsed_nested.append(f"({extract_argument_types(nested)})")
91+
else:
92+
collapsed_nested.append(_get_argument_readable_type(nested))
93+
collapsed_args.append(",".join(collapsed_nested))
94+
else:
95+
collapsed_args.append(_get_argument_readable_type(arg))
96+
97+
return ",".join(collapsed_args)
98+
99+
76100
def find_matching_event_abi(
77101
abi: ABI,
78102
event_name: Optional[str] = None,
@@ -150,12 +174,17 @@ def find_matching_fn_abi(
150174
"Provided arguments can be encoded to multiple functions "
151175
"matching this call."
152176
)
177+
178+
collapsed_args = extract_argument_types(args)
179+
collapsed_kwargs = dict(
180+
{(k, extract_argument_types([v])) for k, v in kwargs.items()}
181+
)
153182
message = (
154183
f"\nCould not identify the intended function with name `{fn_identifier}`, "
155-
f"positional argument(s) of type `{tuple(map(type, args))}` and keyword "
156-
f"argument(s) of type `{valmap(type, kwargs)}`.\nFound "
157-
f"{len(matching_identifiers)} function(s) with the name "
158-
f"`{fn_identifier}`: {matching_function_signatures}{diagnosis}"
184+
f"positional arguments with type(s) `{collapsed_args}` and "
185+
f"keyword arguments with type(s) `{collapsed_kwargs}`."
186+
f"\nFound {len(matching_identifiers)} function(s) with "
187+
f"the name `{fn_identifier}`: {matching_function_signatures}{diagnosis}"
159188
)
160189

161190
raise ValidationError(message)
@@ -333,3 +362,10 @@ def validate_payable(transaction: TxParams, abi: ABIFunction) -> None:
333362
"with payable=False. Please ensure that "
334363
"transaction's value is 0."
335364
)
365+
366+
367+
def _get_argument_readable_type(arg: Any) -> str:
368+
if is_checksum_address(arg) or is_binary_address(arg):
369+
return "address"
370+
371+
return arg.__class__.__name__

0 commit comments

Comments
 (0)