From b47e48c1b1fd8a934dd5e3908804e55c7385e0c9 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 26 Nov 2018 11:07:30 -0500 Subject: [PATCH 1/5] Naive support for tuples Naive in the sense that there's no support for value munging (eg translating an ENS name into its associated address) for values within tuples. Changes to _utils.abi's * is_encodable(), * get_abi_inputs(), and * check_if_arguments_can_be_encoded() are all tested via tests.core.contracts.contract_util_functions.test_find_matching_fn_abi(), since find_matching_fn_abi() uses check_if_arguments_can_be_encoded(), which in turn uses get_abi_inputs() and is_encodable(). Also, get_abi_inputs() has its own test_get_abi_inputs() in that same test module. Finally, check_if_arguments_can_be_encoded() is also exercised by the new test_filter_by_encodability(), which was added for consistency with the other test_filter_by_* tests. Changes to _utils.abi.data_tree_map() are tested via new function tests.core.utilities.test_abi.test_data_tree_map(). Changes to _utils.abi.abi_sub_tree() are tested via a new test case on existing tests.core.utilities.test_map_abi_data(), since map_abi_data() calls abi_data_tree(), which in turn calls abi_sub_tree(). Changes to _utils' * _utils.abi.get_abi_input_types(), * _utils.abi.get_abi_output_types(), and * _utils.contracts.get_function_info() are currently not covered by any tests. --- .../contracts/test_contract_util_functions.py | 39 ++++++ tests/core/utilities/test_abi.py | 105 ++++++++++++++++ .../test_abi_filter_by_encodability.py | 59 +++++++++ tests/core/utilities/test_abi_is_encodable.py | 77 ++++++++++++ web3/_utils/abi.py | 118 +++++++++++++++++- web3/_utils/contracts.py | 3 + 6 files changed, 396 insertions(+), 5 deletions(-) create mode 100644 tests/core/utilities/test_abi_filter_by_encodability.py diff --git a/tests/core/contracts/test_contract_util_functions.py b/tests/core/contracts/test_contract_util_functions.py index c73f8aa66f..596bffab3d 100644 --- a/tests/core/contracts/test_contract_util_functions.py +++ b/tests/core/contracts/test_contract_util_functions.py @@ -1,4 +1,7 @@ +import pytest + from web3.contract import ( + find_matching_fn_abi, parse_block_identifier_int, ) @@ -11,3 +14,39 @@ def test_parse_block_identifier_int(web3): last_num = web3.eth.getBlock('latest').number assert 0 == parse_block_identifier_int(web3, -1 - last_num) + + +@pytest.mark.parametrize( + 'contract_abi, fn_name, args, kwargs, expected', + ( + ( + [ + { + 'inputs': [], + 'type': 'function', + 'name': 'a', + }, + { + 'inputs': [{'type': 'bytes32'}], + 'type': 'function', + 'name': 'a', + }, + { + 'inputs': [{'type': 'uint256'}], + 'type': 'function', + 'name': 'a', + }, + ], + 'a', + [1], + None, + { + 'inputs': [{'type': 'uint256'}], + 'type': 'function', + 'name': 'a', + }, + ), + ), +) +def test_find_matching_fn_abi(fn_name, contract_abi, args, kwargs, expected): + assert expected == find_matching_fn_abi(contract_abi, fn_name, args, kwargs) diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py index 571dd58583..3f0300193b 100644 --- a/tests/core/utilities/test_abi.py +++ b/tests/core/utilities/test_abi.py @@ -2,11 +2,15 @@ import pytest from web3._utils.abi import ( + ABITypedData, abi_data_tree, + data_tree_map, + get_abi_inputs, map_abi_data, ) from web3._utils.normalizers import ( BASE_RETURN_NORMALIZERS, + addresses_checksummed, ) @@ -29,9 +33,45 @@ def test_abi_data_tree(types, data, expected): assert abi_data_tree(types, data) == expected +@pytest.mark.parametrize( + 'func, data_tree, expected', + [ + ( + addresses_checksummed, + [ + ABITypedData( + [ + 'address', + b'\xf2\xe2F\xbbv\xdf\x87l\xef\x8b8\xae\x84\x13\x0fOU\xde9[', + ] + ), + ABITypedData([None, 'latest']) + ], + [ + ABITypedData( + [ + 'address', + '0xF2E246BB76DF876Cef8b38ae84130F4F55De395b', + ] + ), + ABITypedData([None, 'latest']) + ] + ) + ], +) +def test_data_tree_map(func, data_tree, expected): + assert data_tree_map(func, data_tree) == expected + + @pytest.mark.parametrize( 'types, data, funcs, expected', [ + ( # like web3._utils.rpc_abi.RPC_ABIS['eth_getCode'] + ['address', None], + [b'\xf2\xe2F\xbbv\xdf\x87l\xef\x8b8\xae\x84\x13\x0fOU\xde9[', 'latest'], + BASE_RETURN_NORMALIZERS, + ['0xF2E246BB76DF876Cef8b38ae84130F4F55De395b', 'latest'], + ), ( ["bool[2]", "int256"], [[True, False], 9876543210], @@ -57,3 +97,68 @@ def test_abi_data_tree(types, data, expected): ) def test_map_abi_data(types, data, funcs, expected): assert map_abi_data(funcs, types, data) == expected + + +FN_ABI = { + 'inputs': [ + { + 'components': [ + {'name': 'anAddress', 'type': 'address'}, + {'name': 'anInt', 'type': 'uint256'}, + {'name': 'someBytes', 'type': 'bytes'}, + ], + 'type': 'tuple' + }, + ], + 'type': 'function' +} + +FN_ARG_VALUES_AS_DICT = ( + { + 'someBytes': b'0000', + 'anInt': 0, + 'anAddress': '0x' + '0' * 40, + }, +) + +FN_ARG_VALUES_AS_TUPLE = ( + ( + '0x' + '0' * 40, + 0, + b'0000', + ), +) + + +@pytest.mark.parametrize( + 'function_abi, arg_values, expected', + [ + ( + FN_ABI, + FN_ARG_VALUES_AS_DICT, + ( + [ + '(address,uint256,bytes)' + ], + FN_ARG_VALUES_AS_TUPLE, + ), + ), + ( + FN_ABI, + FN_ARG_VALUES_AS_TUPLE, + ( + [ + '(address,uint256,bytes)' + ], + FN_ARG_VALUES_AS_TUPLE, + ), + ), + ( + {'payable': False, 'stateMutability': 'nonpayable', 'type': 'fallback'}, + (), + ([], ()), + ) + ] +) +def test_get_abi_inputs(function_abi, arg_values, expected): + assert get_abi_inputs(function_abi, arg_values) == expected diff --git a/tests/core/utilities/test_abi_filter_by_encodability.py b/tests/core/utilities/test_abi_filter_by_encodability.py new file mode 100644 index 0000000000..d0fda81059 --- /dev/null +++ b/tests/core/utilities/test_abi_filter_by_encodability.py @@ -0,0 +1,59 @@ +import pytest + +from web3._utils.abi import ( + filter_by_encodability, +) + +FN_ABI_ONE_ADDRESS_ARG = {'inputs': [{'name': 'arg', 'type': 'address'}]} + +FN_ABI_MIXED_ARGS = { + 'inputs': [ + { + 'components': [ + {'name': 'anAddress', 'type': 'address'}, + {'name': 'anInt', 'type': 'uint256'}, + {'name': 'someBytes', 'type': 'bytes'}, + ], + 'type': 'tuple' + } + ], + 'type': 'function' +} + + +@pytest.mark.parametrize( + 'arguments,contract_abi,expected_match_count,expected_first_match', + ( + ( + ('0x' + '1' * 40,), + [FN_ABI_ONE_ADDRESS_ARG], + 1, + FN_ABI_ONE_ADDRESS_ARG, + ), + ( + ('0xffff'), # not a valid address + [FN_ABI_ONE_ADDRESS_ARG], + 0, + None, + ), + ( + ( + { + 'anAddress': '0x' + '0' * 40, + 'anInt': 1, + 'someBytes': b'\x00' * 20, + }, + ), + [FN_ABI_MIXED_ARGS], + 1, + FN_ABI_MIXED_ARGS, + ), + ) +) +def test_filter_by_encodability( + arguments, contract_abi, expected_match_count, expected_first_match +): + filter_output = filter_by_encodability(arguments, {}, contract_abi) + assert len(filter_output) == expected_match_count + if expected_match_count > 0: + assert filter_output[0] == expected_first_match diff --git a/tests/core/utilities/test_abi_is_encodable.py b/tests/core/utilities/test_abi_is_encodable.py index 5ac171788b..eb74387faf 100644 --- a/tests/core/utilities/test_abi_is_encodable.py +++ b/tests/core/utilities/test_abi_is_encodable.py @@ -61,6 +61,83 @@ (b'', 'string', True), (b'anything', 'string', True), (b'\x80', 'string', False), # bytes that cannot be decoded with utf-8 are invalid + # tuple + (['0x' + '00' * 20, 0], '(address,uint256)', True), + (('0x' + '00' * 20, 0), '(address,uint256)', True), + ([0], '(address,uint256)', False), + (['0x' + '00' * 20], '(uint256)', False), + ([], '(address)', False), + ((1, (2, 3), 0), '(uint256,(uint256,uint256),uint256)', True), + ((0, (2, 3)), '(uint256,(uint256,uint256))', True), + (((2, 3), 0), '((uint256,uint256),uint256)', True), + ((((0,),),), '(((uint256)))', True), + ([0, 1, 2, 3], 'uint256[]', True), + ([(0, 1), (2, 3)], '(uint256,uint256)[]', True), + ([(0, 1, 2), (3, 4, 5)], '(uint256,uint256,uint256)[]', True), + ( + [0, ['0x' + '00' * 20, '0x' + '00' * 20], ['0x' + '00' * 20, '0x' + '00' * 20], 5], + '(int,(address,address),(address,address),int)', + True, + ), + ( + (0, ('0x' + '00' * 20, '0x' + '00' * 20), ('0x' + '00' * 20, '0x' + '00' * 20), 5), + '(int,(address,address),(address,address),int)', + True, + ), + ( + [ + ['0x' + '00' * 20, '0x' + '00' * 20], + 2, + ['0x' + '00' * 20, '0x' + '00' * 20], + 5, + ['0x' + '00' * 20, '0x' + '00' * 20], + ], + '((address,address),int,(address,address),int,(address,address))', + True, + ), + ( + ( + ('0x' + '00' * 20, '0x' + '00' * 20), + 2, + ('0x' + '00' * 20, '0x' + '00' * 20), + 5, + ('0x' + '00' * 20, '0x' + '00' * 20), + ), + '((address,address),int,(address,address),int,(address,address))', + True, + ), + ( + [0, ['0x' + '00' * 20, [1, 2]], 3], + '(int,(address,(int,int)),int)', + True, + ), + ( + (0, ('0x' + '00' * 20, (1, 2)), 3), + '(int,(address,(int,int)),int)', + True, + ), + ( + [ + [['0x' + '00' * 20], '0x' + '00' * 20], + 1, + ['0x' + '00' * 20, '0x' + '00' * 20], + 2, + [[3, [4, 5]], '0x' + '00' * 20] + ], + '(((address),address),int,(address,address),int,((int,(int,int)),address))', + True, + ), + ( + ( + (('0x' + '00' * 20,), '0x' + '00' * 20), + 1, + ('0x' + '00' * 20, '0x' + '00' * 20), + 2, + ((3, (4, 5)), '0x' + '00' * 20) + ), + '(((address),address),int,(address,address),int,((int,(int,int)),address))', + True, + ), ), ) def test_is_encodable(value, _type, expected): diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index a8acbec79a..d9039d4733 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -7,6 +7,9 @@ from eth_abi import ( is_encodable as eth_abi_is_encodable, ) +from eth_abi.grammar import ( + parse as parse_type_string, +) from eth_utils import ( is_hex, is_list_like, @@ -14,6 +17,9 @@ to_text, to_tuple, ) +from eth_utils.abi import ( + collapse_if_tuple, +) from web3._utils.ens import ( is_ens_name, @@ -51,14 +57,14 @@ def get_abi_input_types(abi): if 'inputs' not in abi and abi['type'] == 'fallback': return [] else: - return [arg['type'] for arg in abi['inputs']] + return [collapse_if_tuple(abi_input) for abi_input in abi['inputs']] def get_abi_output_types(abi): if abi['type'] == 'fallback': return [] else: - return [arg['type'] for arg in abi['outputs']] + return [collapse_if_tuple(arg) for arg in abi['outputs']] def get_abi_input_names(abi): @@ -114,7 +120,6 @@ def filter_by_argument_name(argument_names, contract_abi): ) except ImportError: from eth_abi.grammar import ( - parse as parse_type_string, normalize as normalize_type_string, TupleType, ) @@ -163,6 +168,33 @@ def is_encodable(_type, value): if not isinstance(_type, str): raise ValueError("is_encodable only accepts type strings") + if _type[0] == "(": # it's a tuple. check encodability of each component + if not is_list_like(value): + return False + + if _type.endswith("[]"): + element_type = _type.rstrip("[]") + element_values = value + return all( + [ + is_encodable(element_type, element_value) + for element_value in element_values + ] + ) + + component_types = [str(t) for t in parse_type_string(_type).components] + + if len(component_types) != len(value): + return False + + return all( + [ + is_encodable(component_type, component_value) + for component_type, component_value + in zip(component_types, value) + ] + ) + base, sub, arrlist = process_type(_type) if arrlist: @@ -205,6 +237,69 @@ def filter_by_encodability(args, kwargs, contract_abi): ] +def get_abi_inputs(function_abi, arg_values): + """Similar to get_abi_input_types(), but gets values too. + + Returns a zip of types and their corresponding argument values. + Importantly, looks in `function_abi` for tuples, and for any found, (a) + translates them from the ABI dict representation to the parenthesized type + list representation that's expected by eth_abi, and (b) translates their + corresponding arguments values from the python dict representation to the + tuple representation expected by eth_abi. + + >>> get_abi_inputs( + ... { + ... 'inputs': [ + ... { + ... 'components': [ + ... {'name': 'anAddress', 'type': 'address'}, + ... {'name': 'anInt', 'type': 'uint256'}, + ... {'name': 'someBytes', 'type': 'bytes'} + ... ], + ... 'name': 'arg', + ... 'type': 'tuple' + ... } + ... ], + ... 'type': 'function' + ... }, + ... ( + ... { + ... 'anInt': 12345, + ... 'anAddress': '0x0000000000000000000000000000000000000000', + ... 'someBytes': b'0000', + ... }, + ... ), + ... ) + (['(address,uint256,bytes)'], (('0x0000000000000000000000000000000000000000', 12345, b'0000'),)) + """ + if "inputs" not in function_abi: + return ([], ()) + + types = [] + values = tuple() + for abi_input, arg_value in zip(function_abi["inputs"], arg_values): + if abi_input["type"] == "tuple": + component_types = [] + component_values = [] + for component, value in zip(abi_input["components"], arg_value): + component_types.append(component["type"]) + if isinstance(arg_value, dict): + component_values.append(arg_value[component["name"]]) + elif isinstance(arg_value, tuple): + component_values.append(value) + else: + raise TypeError( + "Unknown value type {} for ABI type 'tuple'" + .format(type(arg_value)) + ) + types.append("(" + ",".join(component_types) + ")") + values += (tuple(component_values),) + else: + types.append(abi_input["type"]) + values += (arg_value,) + return types, values + + def check_if_arguments_can_be_encoded(function_abi, args, kwargs): try: arguments = merge_args_and_kwargs(function_abi, args, kwargs) @@ -214,7 +309,7 @@ def check_if_arguments_can_be_encoded(function_abi, args, kwargs): if len(function_abi.get('inputs', [])) != len(arguments): return False - types = get_abi_input_types(function_abi) + types, arguments = get_abi_inputs(function_abi, arguments) return all( is_encodable(_type, arg) @@ -520,7 +615,13 @@ def data_tree_map(func, data_tree): receive two args: abi_type, and data ''' def map_to_typed_data(elements): - if isinstance(elements, ABITypedData) and elements.abi_type is not None: + if ( + isinstance(elements, ABITypedData) and elements.abi_type is not None and + not ( + isinstance(elements.abi_type, str) and + elements.abi_type[0] == "(" + ) + ): return ABITypedData(func(*elements)) else: return elements @@ -550,6 +651,13 @@ def __new__(cls, iterable): def abi_sub_tree(data_type, data_value): + if ( + isinstance(data_type, str) and + data_type[0] == "(" and + isinstance(data_value, tuple) + ): + return ABITypedData([data_type, data_value]) + if data_type is None: return ABITypedData([None, data_value]) diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 997ca6a8fd..36a2667a49 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -25,6 +25,7 @@ filter_by_name, filter_by_type, get_abi_input_types, + get_abi_inputs, get_fallback_func_abi, map_abi_data, merge_args_and_kwargs, @@ -249,6 +250,8 @@ def get_function_info(fn_name, contract_abi=None, fn_abi=None, args=None, kwargs fn_arguments = merge_args_and_kwargs(fn_abi, args, kwargs) + _, fn_arguments = get_abi_inputs(fn_abi, fn_arguments) + return fn_abi, fn_selector, fn_arguments From 9522b9a74b5d56efaaec197142759370fa115c1b Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Thu, 10 Jan 2019 11:38:21 -0500 Subject: [PATCH 2/5] Exercise doctests in _utils.abi Includes fixes for previously broken doctests. --- tests/core/utilities/test_abi.py | 11 ++++++++++- web3/_utils/abi.py | 8 +++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py index 3f0300193b..56927d5830 100644 --- a/tests/core/utilities/test_abi.py +++ b/tests/core/utilities/test_abi.py @@ -1,6 +1,7 @@ - +import doctest import pytest +import web3._utils.abi from web3._utils.abi import ( ABITypedData, abi_data_tree, @@ -162,3 +163,11 @@ def test_map_abi_data(types, data, funcs, expected): ) def test_get_abi_inputs(function_abi, arg_values, expected): assert get_abi_inputs(function_abi, arg_values) == expected + + +def test_docstrings(capsys): + """Exercise docstrings in the web3._utils.abi module.""" + # disable stdout capture so failed tests will show why they failed + with capsys.disabled(): + failure_count, _ = doctest.testmod(web3._utils.abi) + assert failure_count == 0 diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index d9039d4733..31ef4a67ff 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -599,8 +599,8 @@ def abi_data_tree(types, data): As an example: >>> abi_data_tree(types=["bool[2]", "uint"], data=[[True, False], 0]) - [("bool[2]", [("bool", True), ("bool", False)]), ("uint256", 0)] - ''' + [ABITypedData(abi_type='bool[2]', data=[ABITypedData(abi_type='bool', data=True), ABITypedData(abi_type='bool', data=False)]), ABITypedData(abi_type='uint256', data=0)] + ''' # noqa: E501 (line too long) return [ abi_sub_tree(data_type, data_value) for data_type, data_value @@ -632,9 +632,11 @@ class ABITypedData(namedtuple('ABITypedData', 'abi_type, data')): ''' This class marks data as having a certain ABI-type. + >>> addr1 = "0x" + "0" * 20 + >>> addr2 = "0x" + "f" * 20 >>> a1 = ABITypedData(['address', addr1]) >>> a2 = ABITypedData(['address', addr2]) - >>> addrs = ABITypedData(['address[]', [a1, a2]) + >>> addrs = ABITypedData(['address[]', [a1, a2]]) You can access the fields using tuple() interface, or with attributes: From 2b8d47e280a3f59813e712e90d0fe9b4308d9b30 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Thu, 10 Jan 2019 14:29:47 -0500 Subject: [PATCH 3/5] Renamed some tests for consistency Renamed some tests/core/utilities/test_abi_filter*.py modules so that their names are all consistent. Also, in test_abi_filter_by_argument_name.py, renamed tests.core.utilities.test_abi_filter_by_name.test_filter_by_arguments_1() to ...test_filter_by_argument_name(), to match the function under test. And, in test_abi_filter_by_name.py, renamed test_filter_by_arguments() to test_filter_by_name(), again to match the function under test. --- ..._by_argument_name.py => test_abi_filter_by_argument_name.py} | 2 +- ...est_abi_filter_by_abi_name.py => test_abi_filter_by_name.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/core/utilities/{test_abi_filtering_by_argument_name.py => test_abi_filter_by_argument_name.py} (95%) rename tests/core/utilities/{test_abi_filter_by_abi_name.py => test_abi_filter_by_name.py} (96%) diff --git a/tests/core/utilities/test_abi_filtering_by_argument_name.py b/tests/core/utilities/test_abi_filter_by_argument_name.py similarity index 95% rename from tests/core/utilities/test_abi_filtering_by_argument_name.py rename to tests/core/utilities/test_abi_filter_by_argument_name.py index 2baac9e355..6608c4643e 100644 --- a/tests/core/utilities/test_abi_filtering_by_argument_name.py +++ b/tests/core/utilities/test_abi_filter_by_argument_name.py @@ -55,7 +55,7 @@ (['b'], ['func_3', 'func_4']), ) ) -def test_filter_by_arguments_1(argument_names, expected): +def test_filter_by_argument_name(argument_names, expected): actual_matches = filter_by_argument_name(argument_names, ABI) function_names = [match['name'] for match in actual_matches] assert set(function_names) == set(expected) diff --git a/tests/core/utilities/test_abi_filter_by_abi_name.py b/tests/core/utilities/test_abi_filter_by_name.py similarity index 96% rename from tests/core/utilities/test_abi_filter_by_abi_name.py rename to tests/core/utilities/test_abi_filter_by_name.py index cf7c4d1963..264d4f2078 100644 --- a/tests/core/utilities/test_abi_filter_by_abi_name.py +++ b/tests/core/utilities/test_abi_filter_by_name.py @@ -66,6 +66,6 @@ ('does_not_exist', []), ) ) -def test_filter_by_arguments(name, expected): +def test_filter_by_name(name, expected): actual_matches = filter_by_name(name, ABI) assert actual_matches == expected From 3549fde0900710032d110219b97a7e6a8425acd6 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Fri, 11 Jan 2019 11:50:30 -0500 Subject: [PATCH 4/5] Test passing a tuple into a contract call --- tests/core/contracts/conftest.py | 104 ++++++++++++++++++ .../contracts/test_contract_call_interface.py | 23 ++++ 2 files changed, 127 insertions(+) diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index ca14b2da32..85f56feb9c 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -527,6 +527,110 @@ def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): return web3.eth.contract(**FALLBACK_FUNCTION_CONTRACT) +CONTRACT_TUPLE_SOURCE = """ +pragma experimental ABIEncoderV3; + +contract Tuple { + struct Struct { + int anInt; + bool aBool; + address anAddress; + } + + function method(Struct memory m) + public + pure + returns (Struct memory) + { + return m; + } +}""" + +CONTRACT_TUPLE_CODE = "608060405234801561001057600080fd5b506102ed806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c0100000000000000000000000000000000000000000000000000000000900480634e3269761461004d575b600080fd5b61006760048036036100629190810190610163565b61007d565b60405161007491906101fb565b60405180910390f35b61008561008d565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b60006100d3823561028b565b905092915050565b60006100e7823561029d565b905092915050565b60006100fb82356102a9565b905092915050565b60006060828403121561011557600080fd5b61011f6060610216565b9050600061012f848285016100ef565b6000830152506020610143848285016100db565b6020830152506040610157848285016100c7565b60408301525092915050565b60006060828403121561017557600080fd5b600061018384828501610103565b91505092915050565b61019581610243565b82525050565b6101a481610255565b82525050565b6101b381610261565b82525050565b6060820160008201516101cf60008501826101aa565b5060208201516101e2602085018261019b565b5060408201516101f5604085018261018c565b50505050565b600060608201905061021060008301846101b9565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561023957600080fd5b8060405250919050565b600061024e8261026b565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102968261026b565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a723058203f779dc401e40b86d552e03af453e4f759bd8e4f8f0ca5ffb05909e2af7567b76c6578706572696d656e74616cf50037" # noqa: E501 + +CONTRACT_TUPLE_RUNTIME = "608060405234801561001057600080fd5b5060043610610048576000357c0100000000000000000000000000000000000000000000000000000000900480634e3269761461004d575b600080fd5b61006760048036036100629190810190610163565b61007d565b60405161007491906101fb565b60405180910390f35b61008561008d565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b60006100d3823561028b565b905092915050565b60006100e7823561029d565b905092915050565b60006100fb82356102a9565b905092915050565b60006060828403121561011557600080fd5b61011f6060610216565b9050600061012f848285016100ef565b6000830152506020610143848285016100db565b6020830152506040610157848285016100c7565b60408301525092915050565b60006060828403121561017557600080fd5b600061018384828501610103565b91505092915050565b61019581610243565b82525050565b6101a481610255565b82525050565b6101b381610261565b82525050565b6060820160008201516101cf60008501826101aa565b5060208201516101e2602085018261019b565b5060408201516101f5604085018261018c565b50505050565b600060608201905061021060008301846101b9565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561023957600080fd5b8060405250919050565b600061024e8261026b565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102968261026b565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a723058203f779dc401e40b86d552e03af453e4f759bd8e4f8f0ca5ffb05909e2af7567b76c6578706572696d656e74616cf50037" # noqa: E501 + +CONTRACT_TUPLE_ABI = json.loads(""" +[ + { + "constant": true, + "inputs": [ + { + "components": [ + { + "name": "anInt", + "type": "int256" + }, + { + "name": "aBool", + "type": "bool" + }, + { + "name": "anAddress", + "type": "address" + } + ], + "name": "m", + "type": "tuple" + } + ], + "name": "method", + "outputs": [ + { + "components": [ + { + "name": "anInt", + "type": "int256" + }, + { + "name": "aBool", + "type": "bool" + }, + { + "name": "anAddress", + "type": "address" + } + ], + "name": "", + "type": "tuple" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + } +]""") + + +@pytest.fixture() +def TUPLE_CODE(): + return CONTRACT_TUPLE_CODE + + +@pytest.fixture() +def TUPLE_RUNTIME(): + return CONTRACT_TUPLE_RUNTIME + + +@pytest.fixture() +def TUPLE_ABI(): + return CONTRACT_TUPLE_ABI + + +@pytest.fixture() +def TUPLE_CONTRACT(TUPLE_CODE, TUPLE_RUNTIME, TUPLE_ABI): + return { + 'bytecode': TUPLE_CODE, + 'bytecode_runtime': TUPLE_RUNTIME, + 'abi': TUPLE_ABI, + } + + +@pytest.fixture() +def TupleContract(web3, TUPLE_CONTRACT): + return web3.eth.contract(**TUPLE_CONTRACT) + + class LogFunctions: LogAnonymous = 0 LogNoArguments = 1 diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index 83b4eeadf2..92a5023378 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -134,6 +134,11 @@ def fallback_function_contract(web3, FallballFunctionContract, address_conversio return deploy(web3, FallballFunctionContract, address_conversion_func) +@pytest.fixture() +def tuple_contract(web3, TupleContract, address_conversion_func): + return deploy(web3, TupleContract, address_conversion_func) + + def test_invalid_address_in_deploy_arg(web3, WithConstructorAddressArgumentsContract): with pytest.raises(InvalidAddress): WithConstructorAddressArgumentsContract.constructor( @@ -611,3 +616,21 @@ def test_invalid_fixed_value_reflections(web3, fixed_reflection_contract, functi contract_func = fixed_reflection_contract.functions[function] with pytest.raises(ValidationError, match=error): contract_func(value).call({'gas': 420000}) + + +@pytest.mark.parametrize( + 'method_input, expected', + ( + ( + {'anInt': 0, 'aBool': True, 'anAddress': '0x' + 'f' * 40}, + (0, True, '0x' + 'f' * 40) + ), + ( + (0, True, '0x' + 'f' * 40), + (0, True, '0x' + 'f' * 40), + ), + ) +) +def test_call_tuple_contract(tuple_contract, method_input, expected): + result = tuple_contract.functions.method(method_input).call() + assert result == expected From f5173e851f9be029f48399260d86e972435e8c8e Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 13 Feb 2019 14:15:56 -0500 Subject: [PATCH 5/5] support arrays of tuples --- tests/core/contracts/conftest.py | 65 ++++++++++-- .../contracts/test_contract_call_interface.py | 22 +++- web3/_utils/abi.py | 100 ++++++++++++------ 3 files changed, 148 insertions(+), 39 deletions(-) diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index 85f56feb9c..b75084dd3b 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -528,7 +528,7 @@ def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): CONTRACT_TUPLE_SOURCE = """ -pragma experimental ABIEncoderV3; +pragma experimental ABIEncoderV2; contract Tuple { struct Struct { @@ -536,22 +536,75 @@ def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): bool aBool; address anAddress; } - - function method(Struct memory m) + function methodTakingStruct(Struct memory m) public pure returns (Struct memory) { return m; } + function methodTakingArrayOfStructs(Struct[] memory m) + public + pure + returns (Struct[] memory) + { + return m; + } }""" -CONTRACT_TUPLE_CODE = "608060405234801561001057600080fd5b506102ed806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c0100000000000000000000000000000000000000000000000000000000900480634e3269761461004d575b600080fd5b61006760048036036100629190810190610163565b61007d565b60405161007491906101fb565b60405180910390f35b61008561008d565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b60006100d3823561028b565b905092915050565b60006100e7823561029d565b905092915050565b60006100fb82356102a9565b905092915050565b60006060828403121561011557600080fd5b61011f6060610216565b9050600061012f848285016100ef565b6000830152506020610143848285016100db565b6020830152506040610157848285016100c7565b60408301525092915050565b60006060828403121561017557600080fd5b600061018384828501610103565b91505092915050565b61019581610243565b82525050565b6101a481610255565b82525050565b6101b381610261565b82525050565b6060820160008201516101cf60008501826101aa565b5060208201516101e2602085018261019b565b5060408201516101f5604085018261018c565b50505050565b600060608201905061021060008301846101b9565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561023957600080fd5b8060405250919050565b600061024e8261026b565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102968261026b565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a723058203f779dc401e40b86d552e03af453e4f759bd8e4f8f0ca5ffb05909e2af7567b76c6578706572696d656e74616cf50037" # noqa: E501 +CONTRACT_TUPLE_CODE = "608060405234801561001057600080fd5b50610571806100206000396000f3fe608060405260043610610046576000357c0100000000000000000000000000000000000000000000000000000000900480635442981a1461004b578063d0723aff14610088575b600080fd5b34801561005757600080fd5b50610072600480360361006d919081019061029a565b6100c5565b60405161007f9190610410565b60405180910390f35b34801561009457600080fd5b506100af60048036036100aa91908101906102db565b6100cf565b6040516100bc9190610432565b60405180910390f35b6060819050919050565b6100d76100df565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b6000610125823561050f565b905092915050565b600082601f830112151561014057600080fd5b813561015361014e8261047a565b61044d565b9150818183526020840193506020810190508385606084028201111561017857600080fd5b60005b838110156101a8578161018e88826101da565b84526020840193506060830192505060018101905061017b565b5050505092915050565b60006101be8235610521565b905092915050565b60006101d2823561052d565b905092915050565b6000606082840312156101ec57600080fd5b6101f6606061044d565b90506000610206848285016101c6565b600083015250602061021a848285016101b2565b602083015250604061022e84828501610119565b60408301525092915050565b60006060828403121561024c57600080fd5b610256606061044d565b90506000610266848285016101c6565b600083015250602061027a848285016101b2565b602083015250604061028e84828501610119565b60408301525092915050565b6000602082840312156102ac57600080fd5b600082013567ffffffffffffffff8111156102c657600080fd5b6102d28482850161012d565b91505092915050565b6000606082840312156102ed57600080fd5b60006102fb8482850161023a565b91505092915050565b61030d816104c7565b82525050565b600061031e826104af565b808452602084019350610330836104a2565b60005b82811015610362576103468683516103ce565b61034f826104ba565b9150606086019550600181019050610333565b50849250505092915050565b610377816104d9565b82525050565b610386816104e5565b82525050565b6060820160008201516103a2600085018261037d565b5060208201516103b5602085018261036e565b5060408201516103c86040850182610304565b50505050565b6060820160008201516103e4600085018261037d565b5060208201516103f7602085018261036e565b50604082015161040a6040850182610304565b50505050565b6000602082019050818103600083015261042a8184610313565b905092915050565b6000606082019050610447600083018461038c565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561047057600080fd5b8060405250919050565b600067ffffffffffffffff82111561049157600080fd5b602082029050602081019050919050565b6000602082019050919050565b600081519050919050565b6000602082019050919050565b60006104d2826104ef565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061051a826104ef565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a7230582016790dbe833dd3fd348c5d67805f5fae4b7caa58b6f6cd2e1e146e725d1b09506c6578706572696d656e74616cf50037" # noqa: E501 -CONTRACT_TUPLE_RUNTIME = "608060405234801561001057600080fd5b5060043610610048576000357c0100000000000000000000000000000000000000000000000000000000900480634e3269761461004d575b600080fd5b61006760048036036100629190810190610163565b61007d565b60405161007491906101fb565b60405180910390f35b61008561008d565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b60006100d3823561028b565b905092915050565b60006100e7823561029d565b905092915050565b60006100fb82356102a9565b905092915050565b60006060828403121561011557600080fd5b61011f6060610216565b9050600061012f848285016100ef565b6000830152506020610143848285016100db565b6020830152506040610157848285016100c7565b60408301525092915050565b60006060828403121561017557600080fd5b600061018384828501610103565b91505092915050565b61019581610243565b82525050565b6101a481610255565b82525050565b6101b381610261565b82525050565b6060820160008201516101cf60008501826101aa565b5060208201516101e2602085018261019b565b5060408201516101f5604085018261018c565b50505050565b600060608201905061021060008301846101b9565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561023957600080fd5b8060405250919050565b600061024e8261026b565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102968261026b565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a723058203f779dc401e40b86d552e03af453e4f759bd8e4f8f0ca5ffb05909e2af7567b76c6578706572696d656e74616cf50037" # noqa: E501 +CONTRACT_TUPLE_RUNTIME = "608060405260043610610046576000357c0100000000000000000000000000000000000000000000000000000000900480635442981a1461004b578063d0723aff14610088575b600080fd5b34801561005757600080fd5b50610072600480360361006d919081019061029a565b6100c5565b60405161007f9190610410565b60405180910390f35b34801561009457600080fd5b506100af60048036036100aa91908101906102db565b6100cf565b6040516100bc9190610432565b60405180910390f35b6060819050919050565b6100d76100df565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b6000610125823561050f565b905092915050565b600082601f830112151561014057600080fd5b813561015361014e8261047a565b61044d565b9150818183526020840193506020810190508385606084028201111561017857600080fd5b60005b838110156101a8578161018e88826101da565b84526020840193506060830192505060018101905061017b565b5050505092915050565b60006101be8235610521565b905092915050565b60006101d2823561052d565b905092915050565b6000606082840312156101ec57600080fd5b6101f6606061044d565b90506000610206848285016101c6565b600083015250602061021a848285016101b2565b602083015250604061022e84828501610119565b60408301525092915050565b60006060828403121561024c57600080fd5b610256606061044d565b90506000610266848285016101c6565b600083015250602061027a848285016101b2565b602083015250604061028e84828501610119565b60408301525092915050565b6000602082840312156102ac57600080fd5b600082013567ffffffffffffffff8111156102c657600080fd5b6102d28482850161012d565b91505092915050565b6000606082840312156102ed57600080fd5b60006102fb8482850161023a565b91505092915050565b61030d816104c7565b82525050565b600061031e826104af565b808452602084019350610330836104a2565b60005b82811015610362576103468683516103ce565b61034f826104ba565b9150606086019550600181019050610333565b50849250505092915050565b610377816104d9565b82525050565b610386816104e5565b82525050565b6060820160008201516103a2600085018261037d565b5060208201516103b5602085018261036e565b5060408201516103c86040850182610304565b50505050565b6060820160008201516103e4600085018261037d565b5060208201516103f7602085018261036e565b50604082015161040a6040850182610304565b50505050565b6000602082019050818103600083015261042a8184610313565b905092915050565b6000606082019050610447600083018461038c565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561047057600080fd5b8060405250919050565b600067ffffffffffffffff82111561049157600080fd5b602082029050602081019050919050565b6000602082019050919050565b600081519050919050565b6000602082019050919050565b60006104d2826104ef565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061051a826104ef565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a7230582016790dbe833dd3fd348c5d67805f5fae4b7caa58b6f6cd2e1e146e725d1b09506c6578706572696d656e74616cf50037" # noqa: E501 CONTRACT_TUPLE_ABI = json.loads(""" [ + { + "constant": true, + "inputs": [ + { + "components": [ + { + "name": "anInt", + "type": "int256" + }, + { + "name": "aBool", + "type": "bool" + }, + { + "name": "anAddress", + "type": "address" + } + ], + "name": "m", + "type": "tuple[]" + } + ], + "name": "methodTakingArrayOfStructs", + "outputs": [ + { + "components": [ + { + "name": "anInt", + "type": "int256" + }, + { + "name": "aBool", + "type": "bool" + }, + { + "name": "anAddress", + "type": "address" + } + ], + "name": "", + "type": "tuple[]" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, { "constant": true, "inputs": [ @@ -574,7 +627,7 @@ def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): "type": "tuple" } ], - "name": "method", + "name": "methodTakingStruct", "outputs": [ { "components": [ diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index 92a5023378..ea245a657b 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -631,6 +631,24 @@ def test_invalid_fixed_value_reflections(web3, fixed_reflection_contract, functi ), ) ) -def test_call_tuple_contract(tuple_contract, method_input, expected): - result = tuple_contract.functions.method(method_input).call() +def test_call_tuple_contract_struct(tuple_contract, method_input, expected): + result = tuple_contract.functions.methodTakingStruct(method_input).call() + assert result == expected + + +@pytest.mark.parametrize( + 'method_input, expected', + ( + ( + [{'anInt': 0, 'aBool': True, 'anAddress': '0x' + 'f' * 40}], + ((0, True, '0x' + 'f' * 40),) + ), + ( + [(0, True, '0x' + 'f' * 40)], + ((0, True, '0x' + 'f' * 40),) + ), + ) +) +def test_call_tuple_contract_struct_array(tuple_contract, method_input, expected): + result = tuple_contract.functions.methodTakingArrayOfStructs(method_input).call() assert result == expected diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 31ef4a67ff..e026fc7b59 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -121,30 +121,23 @@ def filter_by_argument_name(argument_names, contract_abi): except ImportError: from eth_abi.grammar import ( normalize as normalize_type_string, - TupleType, ) def process_type(type_str): normalized_type_str = normalize_type_string(type_str) abi_type = parse_type_string(normalized_type_str) - if isinstance(abi_type, TupleType): - type_str_repr = repr(type_str) - if type_str != normalized_type_str: - type_str_repr = '{} (normalized to {})'.format( - type_str_repr, - repr(normalized_type_str), - ) - - raise ValueError( - "Cannot process type {}: tuple types not supported".format( - type_str_repr, - ) - ) - abi_type.validate() - sub = abi_type.sub + if hasattr(abi_type, 'base'): + base = abi_type.base + else: + base = str(abi_type.item_type) + + if hasattr(abi_type, 'sub'): + sub = abi_type.sub + else: + sub = None if isinstance(sub, tuple): sub = 'x'.join(map(str, sub)) elif isinstance(sub, int): @@ -158,7 +151,7 @@ def process_type(type_str): else: arrlist = [] - return abi_type.base, sub, arrlist + return base, sub, arrlist def collapse_type(base, sub, arrlist): return base + str(sub) + ''.join(map(repr, arrlist)) @@ -275,23 +268,68 @@ def get_abi_inputs(function_abi, arg_values): if "inputs" not in function_abi: return ([], ()) + def collate_tuple_components(components, values): + """Collates tuple components with their values. + + :param components: is an array of ABI components, such as one extracted + from an input element of a function ABI. + :param values: can be any of a list, tuple, or dict. If a dict, key + names must correspond to component names specified in the components + parameter. If a list or array, the order of the elements should + correspond to the order of elements in the components array. + + Returns a two-element tuple. The first element is a string comprised + of the parenthesized list of tuple component types. The second element + is a tuple of the values corresponding to the types in the first + element. + + >>> collate_tuple_components( + ... [ + ... {'name': 'anAddress', 'type': 'address'}, + ... {'name': 'anInt', 'type': 'uint256'}, + ... {'name': 'someBytes', 'type': 'bytes'} + ... ], + ... ( + ... { + ... 'anInt': 12345, + ... 'anAddress': '0x0000000000000000000000000000000000000000', + ... 'someBytes': b'0000', + ... }, + ... ), + ... ) + + """ + component_types = [] + component_values = [] + for component, value in zip(components, values): + component_types.append(component["type"]) + if isinstance(values, dict): + component_values.append(values[component["name"]]) + elif is_list_like(values): + component_values.append(value) + else: + raise TypeError( + "Unknown value type {} for ABI type 'tuple'" + .format(type(values)) + ) + return component_types, component_values + types = [] values = tuple() for abi_input, arg_value in zip(function_abi["inputs"], arg_values): - if abi_input["type"] == "tuple": - component_types = [] - component_values = [] - for component, value in zip(abi_input["components"], arg_value): - component_types.append(component["type"]) - if isinstance(arg_value, dict): - component_values.append(arg_value[component["name"]]) - elif isinstance(arg_value, tuple): - component_values.append(value) - else: - raise TypeError( - "Unknown value type {} for ABI type 'tuple'" - .format(type(arg_value)) - ) + if abi_input["type"] == "tuple[]": + value_array = [] + for arg_arr_elem_val in arg_value: + component_types, component_values = collate_tuple_components( + abi_input["components"], arg_arr_elem_val + ) + value_array.append(component_values) + types.append("(" + ",".join(component_types) + ")[]") + values += (value_array,) + elif abi_input["type"] == "tuple": + component_types, component_values = collate_tuple_components( + abi_input["components"], arg_value + ) types.append("(" + ",".join(component_types) + ")") values += (tuple(component_values),) else: