Skip to content

Commit 04180ef

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 d56cf6e commit 04180ef

File tree

3 files changed

+93
-9
lines changed

3 files changed

+93
-9
lines changed

tests/core/contracts/test_contract_call_interface.py

+50-4
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,8 @@ def test_returns_data_from_specified_block(w3, math_contract):
573573

574574

575575
message_regex = (
576-
r"\nCould not identify the intended function with name `.*`, "
577-
r"positional argument\(s\) of type `.*` and "
578-
r"keyword argument\(s\) of type `.*`."
576+
r"\nCould not identify the intended function with name `.*`, positional arguments "
577+
r"with type\(s\) `.*` and keyword arguments with type\(s\) `.*`."
579578
r"\nFound .* function\(s\) with the name `.*`: .*"
580579
)
581580
diagnosis_arg_regex = (
@@ -625,6 +624,52 @@ def test_function_multiple_error_diagnoses(w3, arg1, arg2, diagnosis):
625624
Contract.functions.a(arg1).call()
626625

627626

627+
@pytest.mark.parametrize(
628+
"address", (
629+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # checksummed
630+
b'\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee', # noqa: E501
631+
)
632+
)
633+
def test_function_wrong_args_for_tuple_collapses_args_in_message(
634+
address, tuple_contract,
635+
):
636+
with pytest.raises(ValidationError) as e:
637+
tuple_contract.functions.method(
638+
(1, [2, 3], [(4, [True, [False]], [address])])
639+
).call()
640+
641+
# assert the user arguments are formatted as expected:
642+
# (int,(int,int),((int,(bool,(bool)),(address))))
643+
e.match("\\(int,\\(int,int\\),\\(\\(int,\\(bool,\\(bool\\)\\),\\(address\\)\\)\\)\\)") # noqa: E501
644+
645+
# assert the found method signature is formatted as expected:
646+
# ['method((uint256,uint256[],(int256,bool[2],address[])[]))']
647+
e.match("\\['method\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)'\\]") # noqa: E501
648+
649+
650+
@pytest.mark.parametrize(
651+
"address", (
652+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # checksummed
653+
b'\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee', # noqa: E501
654+
)
655+
)
656+
def test_function_wrong_args_for_tuple_collapses_kwargs_in_message(
657+
address, tuple_contract
658+
):
659+
with pytest.raises(ValidationError) as e:
660+
tuple_contract.functions.method(
661+
a=(1, [2, 3], [(4, [True, [False]], [address])]) # noqa: E501
662+
).call()
663+
664+
# assert the user keyword arguments are formatted as expected:
665+
# {'a': '(int,(int,int),((int,(bool,(bool)),(address))))'}
666+
e.match("{'a': '\\(int,\\(int,int\\),\\(\\(int,\\(bool,\\(bool\\)\\),\\(address\\)\\)\\)\\)'}") # noqa: E501
667+
668+
# assert the found method signature is formatted as expected:
669+
# ['method((uint256,uint256[],(int256,bool[2],address[])[]))']
670+
e.match("\\['method\\(\\(uint256,uint256\\[\\],\\(int256,bool\\[2\\],address\\[\\]\\)\\[\\]\\)\\)'\\]") # noqa: E501
671+
672+
628673
def test_function_no_abi(w3):
629674
contract = w3.eth.contract()
630675
with pytest.raises(NoABIFound):
@@ -1291,7 +1336,8 @@ async def test_async_no_functions_match_identifier(async_arrays_contract):
12911336

12921337
@pytest.mark.asyncio
12931338
async def test_async_function_1_match_identifier_wrong_number_of_args(
1294-
async_arrays_contract):
1339+
async_arrays_contract
1340+
):
12951341
regex = message_regex + diagnosis_arg_regex
12961342
with pytest.raises(ValidationError, match=regex):
12971343
await async_arrays_contract.functions.setBytes32Value().call()

web3/_utils/abi.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,8 @@ def abi_to_signature(abi: Union[ABIFunction, ABIEvent]) -> str:
719719
function_signature = "{fn_name}({fn_input_types})".format(
720720
fn_name=abi["name"],
721721
fn_input_types=",".join(
722-
[arg["type"] for arg in normalize_event_input_types(abi.get("inputs", []))]
722+
collapse_if_tuple(dict(arg))
723+
for arg in normalize_event_input_types(abi.get("inputs", []))
723724
),
724725
)
725726
return function_signature

web3/_utils/contracts.py

+41-4
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,
@@ -149,10 +173,16 @@ def find_matching_fn_abi(
149173
"\nAmbiguous argument encoding. "
150174
"Provided arguments can be encoded to multiple functions matching this call."
151175
)
176+
177+
collapsed_args = extract_argument_types(args)
178+
collapsed_kwargs = dict(
179+
{(k, extract_argument_types([v])) for k, v in kwargs.items()}
180+
)
152181
message = (
153-
f"\nCould not identify the intended function with name `{fn_identifier}`, positional "
154-
f"argument(s) of type `{tuple(map(type, args))}` and keyword argument(s) of type "
155-
f"`{valmap(type, kwargs)}`.\nFound {len(matching_identifiers)} function(s) with "
182+
f"\nCould not identify the intended function with name `{fn_identifier}`, "
183+
f"positional arguments with type(s) `{collapsed_args}` and "
184+
f"keyword arguments with type(s) `{collapsed_kwargs}`."
185+
f"\nFound {len(matching_identifiers)} function(s) with "
156186
f"the name `{fn_identifier}`: {matching_function_signatures}{diagnosis}"
157187
)
158188

@@ -331,3 +361,10 @@ def validate_payable(transaction: TxParams, abi: ABIFunction) -> None:
331361
"with payable=False. Please ensure that "
332362
"transaction's value is 0."
333363
)
364+
365+
366+
def _get_argument_readable_type(arg: Any) -> str:
367+
if is_checksum_address(arg) or is_binary_address(arg):
368+
return "address"
369+
370+
return arg.__class__.__name__

0 commit comments

Comments
 (0)