From e8f42aa790fd376498b3503135d08d43d81fc323 Mon Sep 17 00:00:00 2001 From: banteg Date: Mon, 13 May 2019 13:37:39 -0600 Subject: [PATCH 01/23] Add rich tuple decoder --- web3/_utils/abi.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 851fcd5dca..b2fd742692 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -711,3 +711,25 @@ def strip_abi_type(elements): return elements.data else: return elements + + +def named_data_tree(abi, data): + """ + Turn tuple into a rich dict. Useful if you deal with named output values like Struct. + """ + abi_type = parse(collapse_if_tuple(abi)) + name = abi['name'] + + if abi_type.is_array: + item_type = abi_type.item_type.to_type_str() + item_abi = {**abi, 'type': item_type, 'name': ''} + result = {name: [named_data_tree(item_abi, item) for item in data]} + elif isinstance(abi_type, TupleType): + result = {name: {}} + components = [named_data_tree(a, b) for a, b in zip(abi['components'], data)] + for item in components: + result[name].update(item) + else: + result = {name: data} + + return result.get('', result) From 3891f4fc5c456e173de1bb820b5b680c7613d502 Mon Sep 17 00:00:00 2001 From: banteg Date: Tue, 14 May 2019 19:12:15 +0700 Subject: [PATCH 02/23] Add decode option to contract --- web3/contract.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/web3/contract.py b/web3/contract.py index d04fff9337..6c2e1ef087 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -36,6 +36,7 @@ is_array_type, map_abi_data, merge_args_and_kwargs, + named_data_tree, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, @@ -99,7 +100,7 @@ class ContractFunctions: """Class containing contract function objects """ - def __init__(self, abi, web3, address=None): + def __init__(self, abi, web3, address=None, decode=False): self.abi = abi self.web3 = web3 self.address = address @@ -115,7 +116,8 @@ def __init__(self, abi, web3, address=None): web3=self.web3, contract_abi=self.abi, address=self.address, - function_identifier=func['name'])) + function_identifier=func['name'], + decode=decode)) def __iter__(self): if not hasattr(self, '_functions') or not self._functions: @@ -243,6 +245,7 @@ class Contract: functions = None caller = None + decode = None #: Instance of :class:`ContractEvents` presenting available Event ABIs events = None @@ -272,8 +275,8 @@ def __init__(self, address=None): if not self.address: raise TypeError("The address argument is required to instantiate a contract.") - self.functions = ContractFunctions(self.abi, self.web3, self.address) - self.caller = ContractCaller(self.abi, self.web3, self.address) + self.functions = ContractFunctions(self.abi, self.web3, self.address, decode=self.decode) + self.caller = ContractCaller(self.abi, self.web3, self.address, decode=self.decode) self.events = ContractEvents(self.abi, self.web3, self.address) self.fallback = Contract.get_fallback_function(self.abi, self.web3, self.address) @@ -295,6 +298,7 @@ def factory(cls, web3, class_name=None, **kwargs): kwargs, normalizers=normalizers, ) + contract.decode = kwargs.get('decode', False) contract.functions = ContractFunctions(contract.abi, contract.web3) contract.caller = ContractCaller(contract.abi, contract.web3, contract.address) contract.events = ContractEvents(contract.abi, contract.web3) @@ -722,6 +726,7 @@ class ContractFunction: abi = None transaction = None arguments = None + decode = None def __init__(self, abi=None): self.abi = abi @@ -819,6 +824,7 @@ def call(self, transaction=None, block_identifier='latest'): block_id, self.contract_abi, self.abi, + self.decode, *self.args, **self.kwargs ) @@ -1192,11 +1198,13 @@ def __init__(self, web3, address, transaction=None, - block_identifier='latest'): + block_identifier='latest', + decode=False): self.web3 = web3 self.address = address self.abi = abi self._functions = None + self.decode = decode if self.abi: if transaction is None: @@ -1209,7 +1217,8 @@ def __init__(self, web3=self.web3, contract_abi=self.abi, address=self.address, - function_identifier=func['name']) + function_identifier=func['name'], + decode=decode) block_id = parse_block_identifier(self.web3, block_identifier) caller_method = partial(self.call_function, @@ -1247,7 +1256,8 @@ def __call__(self, transaction=None, block_identifier='latest'): self.web3, self.address, transaction=transaction, - block_identifier=block_identifier) + block_identifier=block_identifier, + decode=self.decode) @staticmethod def call_function(fn, *args, transaction=None, block_identifier='latest', **kwargs): @@ -1281,6 +1291,7 @@ def call_contract_function( block_id=None, contract_abi=None, fn_abi=None, + decode=False, *args, **kwargs): """ @@ -1339,6 +1350,12 @@ def call_contract_function( ) normalized_data = map_abi_data(_normalizers, output_types, output_data) + if decode: + normalized_data = [ + named_data_tree(abi, data) for abi, data + in zip(fn_abi['outputs'], normalized_data) + ] + if len(normalized_data) == 1: return normalized_data[0] else: From 7e24a3abd3bb2d7fb2230d81f4c1a848e89a9f5c Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 15 May 2019 07:20:56 +0700 Subject: [PATCH 03/23] Add tests for named tuple decoder --- tests/core/utilities/test_abi_named_tree.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/core/utilities/test_abi_named_tree.py diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py new file mode 100644 index 0000000000..f4ea75dbb3 --- /dev/null +++ b/tests/core/utilities/test_abi_named_tree.py @@ -0,0 +1,28 @@ +from web3._utils.abi import ( + check_if_arguments_can_be_encoded, + named_data_tree, +) + +from .test_abi import ( + TEST_FUNCTION_ABI, +) + +abi = TEST_FUNCTION_ABI['inputs'] +inputs = ( + (1, [2, 3, 4], [(5, 6), (7, 8), (9, 10)]), # Value for s + (11, 12), # Value for t + 13, # Value for a +) +expected = [ + {'s': {'a': 1, 'b': [2, 3, 4], 'c': [{'x': 5, 'y': 6}, {'x': 7, 'y': 8}, {'x': 9, 'y': 10}]}}, + {'t': {'x': 11, 'y': 12}}, + {'a': 13}, +] + + +def test_named_data_tree(): + result = [named_data_tree(a, b) for a, b in zip(abi, inputs)] + assert result == expected + + merged = {key: item[key] for item in result for key in item} + assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, (), merged) From 1bdd96427ec618c38e34fd46c8e345ccb0ff9bb8 Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 15 May 2019 09:52:28 +0700 Subject: [PATCH 04/23] Use dict comprehension when building decoded tuple --- web3/_utils/abi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index b2fd742692..3fb09e3e7a 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -725,10 +725,8 @@ def named_data_tree(abi, data): item_abi = {**abi, 'type': item_type, 'name': ''} result = {name: [named_data_tree(item_abi, item) for item in data]} elif isinstance(abi_type, TupleType): - result = {name: {}} components = [named_data_tree(a, b) for a, b in zip(abi['components'], data)] - for item in components: - result[name].update(item) + result = {name: {key: item[key] for item in components for key in item}} else: result = {name: data} From 488a194262966969900465926f7d5d550eaf302f Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 15 May 2019 14:52:28 +0700 Subject: [PATCH 05/23] Fix tuple decoding in decode_function_input --- web3/contract.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web3/contract.py b/web3/contract.py index 6c2e1ef087..5fc5c00948 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -31,6 +31,7 @@ check_if_arguments_can_be_encoded, fallback_func_abi_exists, filter_by_type, + get_abi_input_types, get_abi_output_types, get_constructor_abi, is_array_type, @@ -394,11 +395,12 @@ def decode_function_input(self, data): data = HexBytes(data) selector, params = data[:4], data[4:] func = self.get_function_by_selector(selector) - names = [x['name'] for x in func.abi['inputs']] - types = [x['type'] for x in func.abi['inputs']] + types = get_abi_input_types(func.abi) decoded = decode_abi(types, params) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) - return func, dict(zip(names, normalized)) + args = [named_data_tree(a, b) for a, b in zip(func.abi['inputs'], normalized)] + result = {key: item[key] for item in args for key in item} + return func, result @combomethod def find_functions_by_args(self, *args): From 147d98b7e72865fb10498456d32f02333447fc2d Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 18:43:18 +0700 Subject: [PATCH 06/23] Decode tuples as namedtuples instead of dicts --- web3/_utils/abi.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 3fb09e3e7a..ce44713769 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -715,7 +715,7 @@ def strip_abi_type(elements): def named_data_tree(abi, data): """ - Turn tuple into a rich dict. Useful if you deal with named output values like Struct. + Convert tuple into a named tuple. Useful if you deal with named output values like structs. """ abi_type = parse(collapse_if_tuple(abi)) name = abi['name'] @@ -723,11 +723,12 @@ def named_data_tree(abi, data): if abi_type.is_array: item_type = abi_type.item_type.to_type_str() item_abi = {**abi, 'type': item_type, 'name': ''} - result = {name: [named_data_tree(item_abi, item) for item in data]} - elif isinstance(abi_type, TupleType): - components = [named_data_tree(a, b) for a, b in zip(abi['components'], data)] - result = {name: {key: item[key] for item in components for key in item}} - else: - result = {name: data} + items = [named_data_tree(item_abi, item) for item in data] + return items + + if isinstance(abi_type, TupleType): + items = [named_data_tree(*item) for item in zip(abi['components'], data)] + names = [item['name'] for item in abi['components']] + return namedtuple('Tuple', names)(*items) - return result.get('', result) + return data From 70790d3c527522cbc87186b476fa27ce00c99bc6 Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 20:43:15 +0700 Subject: [PATCH 07/23] Add foldable namedtuple such that type(x)(x) == x --- tests/core/utilities/test_abi_named_tree.py | 39 +++++++++++++-------- web3/_utils/abi.py | 12 ++++++- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index f4ea75dbb3..c256473012 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -1,28 +1,37 @@ from web3._utils.abi import ( check_if_arguments_can_be_encoded, + foldable_namedtuple, named_data_tree, ) -from .test_abi import ( - TEST_FUNCTION_ABI, -) +from .test_abi import TEST_FUNCTION_ABI abi = TEST_FUNCTION_ABI['inputs'] + +# s = (a=1, b=[2, 3, 4], c=[(x=5, y=6), (x=7, y=8), (x=9, y=10)]) +# t = (x=11, y=12) +# a = 13 inputs = ( - (1, [2, 3, 4], [(5, 6), (7, 8), (9, 10)]), # Value for s - (11, 12), # Value for t - 13, # Value for a + (1, [2, 3, 4], [(5, 6), (7, 8), (9, 10)]), + (11, 12), + 13, ) -expected = [ - {'s': {'a': 1, 'b': [2, 3, 4], 'c': [{'x': 5, 'y': 6}, {'x': 7, 'y': 8}, {'x': 9, 'y': 10}]}}, - {'t': {'x': 11, 'y': 12}}, - {'a': 13}, -] def test_named_data_tree(): - result = [named_data_tree(a, b) for a, b in zip(abi, inputs)] - assert result == expected + s, t, a = [named_data_tree(*item) for item in zip(abi, inputs)] + assert (s, t, a) == inputs + assert s.c[2].y == 10 + assert t.x == 11 + assert a == 13 + + +def test_namedtuples_encodable(): + args = [named_data_tree(*item) for item in zip(abi, inputs)] + assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, args, {}) + - merged = {key: item[key] for item in result for key in item} - assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, (), merged) +def test_foldable_namedtuple(): + item = foldable_namedtuple('a b c')([1, 2, 3]) + assert type(item)(item) == item == (1, 2, 3) + assert item.c == 3 diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index ce44713769..bc64e8aab5 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -729,6 +729,16 @@ def named_data_tree(abi, data): if isinstance(abi_type, TupleType): items = [named_data_tree(*item) for item in zip(abi['components'], data)] names = [item['name'] for item in abi['components']] - return namedtuple('Tuple', names)(*items) + return foldable_namedtuple(names)(items) return data + + +def foldable_namedtuple(fields): + """ + Customized namedtuple such that `type(x)(x) == x`. + """ + class Tuple(namedtuple('Tuple', fields)): + def __new__(self, args): + return super().__new__(self, *args) + return Tuple From aa8bddbe2d914c56d949238ce8efffadfd3f7324 Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 21:06:13 +0700 Subject: [PATCH 08/23] Add decode_arguments function that deals with top-level names --- web3/_utils/abi.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index bc64e8aab5..58ed80bb03 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -713,12 +713,20 @@ def strip_abi_type(elements): return elements -def named_data_tree(abi, data): +def decode_arguments(abi, data): """ - Convert tuple into a named tuple. Useful if you deal with named output values like structs. + Convert function inputs/outputs tuple to named tuple using names from ABI. + Useful when dealing with structs. The output of this function is accepted where tuples work. + + ABI argument should be fn_abi['inputs'] or fn_abi['outputs'] """ + decoded = [named_data_tree(*item) for item in zip(abi, data)] + fields = [item['name'] for item in abi] + return foldable_namedtuple(fields)(decoded) if all(fields) else decode + + +def named_data_tree(abi, data): abi_type = parse(collapse_if_tuple(abi)) - name = abi['name'] if abi_type.is_array: item_type = abi_type.item_type.to_type_str() From fcbc528b8be800303ba8e6c1ff4793d1b1fc0fab Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 21:06:44 +0700 Subject: [PATCH 09/23] Use named_arguments_tuple for decoding function inputs/outputs --- tests/core/utilities/test_abi_named_tree.py | 17 +++++++++-------- web3/_utils/abi.py | 2 +- web3/contract.py | 11 ++++------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index c256473012..a7282edca7 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -1,7 +1,7 @@ from web3._utils.abi import ( check_if_arguments_can_be_encoded, foldable_namedtuple, - named_data_tree, + named_arguments_tuple, ) from .test_abi import TEST_FUNCTION_ABI @@ -18,16 +18,17 @@ ) -def test_named_data_tree(): - s, t, a = [named_data_tree(*item) for item in zip(abi, inputs)] - assert (s, t, a) == inputs - assert s.c[2].y == 10 - assert t.x == 11 - assert a == 13 +def test_named_arguments_decode(): + data = named_arguments_tuple(abi, inputs) + s, t, a = data + assert data == inputs + assert data.s.c[2].y == 10 + assert data.t.x == 11 + assert data.a == 13 def test_namedtuples_encodable(): - args = [named_data_tree(*item) for item in zip(abi, inputs)] + args = named_arguments_tuple(abi, inputs) assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, args, {}) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 58ed80bb03..9acd9c1a33 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -713,7 +713,7 @@ def strip_abi_type(elements): return elements -def decode_arguments(abi, data): +def named_arguments_tuple(abi, data): """ Convert function inputs/outputs tuple to named tuple using names from ABI. Useful when dealing with structs. The output of this function is accepted where tuples work. diff --git a/web3/contract.py b/web3/contract.py index 5fc5c00948..e85c21dc7f 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -29,6 +29,7 @@ from web3._utils.abi import ( abi_to_signature, check_if_arguments_can_be_encoded, + decode_arguments, fallback_func_abi_exists, filter_by_type, get_abi_input_types, @@ -398,9 +399,8 @@ def decode_function_input(self, data): types = get_abi_input_types(func.abi) decoded = decode_abi(types, params) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) - args = [named_data_tree(a, b) for a, b in zip(func.abi['inputs'], normalized)] - result = {key: item[key] for item in args for key in item} - return func, result + args = decode_arguments(func.abi['inputs'], normalized) + return func, args @combomethod def find_functions_by_args(self, *args): @@ -1353,10 +1353,7 @@ def call_contract_function( normalized_data = map_abi_data(_normalizers, output_types, output_data) if decode: - normalized_data = [ - named_data_tree(abi, data) for abi, data - in zip(fn_abi['outputs'], normalized_data) - ] + normalized_data = decode_arguments(fn_abi['outputs'], normalized_data) if len(normalized_data) == 1: return normalized_data[0] From 822ad6437632ebcd7739713d81b4020f0c47b9cd Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 21:38:06 +0700 Subject: [PATCH 10/23] Move decode_transaction_data to utils, make it more testable --- web3/_utils/contracts.py | 11 +++++++++++ web3/contract.py | 11 ++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 90ecc17e93..9a073b6991 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -2,6 +2,7 @@ from eth_abi import ( encode_abi as eth_abi_encode_abi, + decode_abi, ) from eth_utils import ( add_0x_prefix, @@ -30,6 +31,7 @@ get_fallback_func_abi, map_abi_data, merge_args_and_kwargs, + named_arguments_tuple, ) from web3._utils.encoding import ( to_hex, @@ -220,6 +222,15 @@ def encode_transaction_data( return add_0x_prefix(encode_abi(web3, fn_abi, fn_arguments, fn_selector)) +def decode_transaction_data(fn_abi, data, normalizers=None): + data = HexBytes(data) + selector, params = data[:4], data[4:] + types = get_abi_input_types(fn_abi) + decoded = decode_abi(types, params) + decoded = map_abi_data(normalizers, types, decoded) + return named_arguments_tuple(fn_abi['inputs'], decoded) + + def get_fallback_function_info(contract_abi=None, fn_abi=None): if fn_abi is None: fn_abi = get_fallback_func_abi(contract_abi) diff --git a/web3/contract.py b/web3/contract.py index e85c21dc7f..299c225b10 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -29,7 +29,6 @@ from web3._utils.abi import ( abi_to_signature, check_if_arguments_can_be_encoded, - decode_arguments, fallback_func_abi_exists, filter_by_type, get_abi_input_types, @@ -38,12 +37,13 @@ is_array_type, map_abi_data, merge_args_and_kwargs, - named_data_tree, + named_arguments_tuple, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, ) from web3._utils.contracts import ( + decode_transaction_data, encode_abi, find_matching_event_abi, find_matching_fn_abi, @@ -396,11 +396,8 @@ def decode_function_input(self, data): data = HexBytes(data) selector, params = data[:4], data[4:] func = self.get_function_by_selector(selector) - types = get_abi_input_types(func.abi) - decoded = decode_abi(types, params) - normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) - args = decode_arguments(func.abi['inputs'], normalized) - return func, args + arguments = decode_transaction_data(func.abi, data, normalizers=BASE_RETURN_NORMALIZERS) + return func, arguments @combomethod def find_functions_by_args(self, *args): From 3c08f1e4b08abaddb5926ba3c854a58081c6c20f Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 21:59:21 +0700 Subject: [PATCH 11/23] Add tests for decoding transaction data --- .../utilities/test_decode_transaction_data.py | 16 ++++++++++++++++ web3/_utils/abi.py | 2 +- web3/_utils/contracts.py | 6 +++--- web3/contract.py | 6 ++---- 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 tests/core/utilities/test_decode_transaction_data.py diff --git a/tests/core/utilities/test_decode_transaction_data.py b/tests/core/utilities/test_decode_transaction_data.py new file mode 100644 index 0000000000..5c4e3f56fc --- /dev/null +++ b/tests/core/utilities/test_decode_transaction_data.py @@ -0,0 +1,16 @@ +from web3._utils.contracts import ( + encode_transaction_data, + decode_transaction_data, +) + +from .test_abi import ( + TEST_FUNCTION_ABI, + GET_ABI_INPUTS_OUTPUT, +) + + +def test_decode_transaction_data(): + fn_abi = TEST_FUNCTION_ABI + args = GET_ABI_INPUTS_OUTPUT[1] + data = encode_transaction_data(None, 'f', fn_abi=fn_abi, args=args) + assert decode_transaction_data(TEST_FUNCTION_ABI, data) == args diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 9acd9c1a33..d3569a7b7d 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -722,7 +722,7 @@ def named_arguments_tuple(abi, data): """ decoded = [named_data_tree(*item) for item in zip(abi, data)] fields = [item['name'] for item in abi] - return foldable_namedtuple(fields)(decoded) if all(fields) else decode + return foldable_namedtuple(fields)(decoded) if all(fields) else decoded def named_data_tree(abi, data): diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 9a073b6991..7d89fe9f0f 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -224,10 +224,10 @@ def encode_transaction_data( def decode_transaction_data(fn_abi, data, normalizers=None): data = HexBytes(data) - selector, params = data[:4], data[4:] types = get_abi_input_types(fn_abi) - decoded = decode_abi(types, params) - decoded = map_abi_data(normalizers, types, decoded) + decoded = decode_abi(types, data[4:]) + if normalizers: + decoded = map_abi_data(normalizers, types, decoded) return named_arguments_tuple(fn_abi['inputs'], decoded) diff --git a/web3/contract.py b/web3/contract.py index 299c225b10..2bcc91d215 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -31,7 +31,6 @@ check_if_arguments_can_be_encoded, fallback_func_abi_exists, filter_by_type, - get_abi_input_types, get_abi_output_types, get_constructor_abi, is_array_type, @@ -394,8 +393,7 @@ def callable_check(fn_abi): @combomethod def decode_function_input(self, data): data = HexBytes(data) - selector, params = data[:4], data[4:] - func = self.get_function_by_selector(selector) + func = self.get_function_by_selector(data[:4]) arguments = decode_transaction_data(func.abi, data, normalizers=BASE_RETURN_NORMALIZERS) return func, arguments @@ -1350,7 +1348,7 @@ def call_contract_function( normalized_data = map_abi_data(_normalizers, output_types, output_data) if decode: - normalized_data = decode_arguments(fn_abi['outputs'], normalized_data) + normalized_data = named_arguments_tuple(fn_abi['outputs'], normalized_data) if len(normalized_data) == 1: return normalized_data[0] From 1e42dda18a37cc8e41919be13308969f090813ab Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 22:22:16 +0700 Subject: [PATCH 12/23] Make tuples anonymous --- web3/_utils/abi.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index d3569a7b7d..ae23a2ed12 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -749,4 +749,9 @@ def foldable_namedtuple(fields): class Tuple(namedtuple('Tuple', fields)): def __new__(self, args): return super().__new__(self, *args) + + def __repr__(self): + repr_fmt = '(' + ', '.join(f'{name}=%r' for name in self._fields) + ')' + return repr_fmt % self + return Tuple From ade066918e7d5c0ef32b359dd8e796b2101c113e Mon Sep 17 00:00:00 2001 From: banteg Date: Thu, 16 May 2019 22:26:26 +0700 Subject: [PATCH 13/23] Rediscover old tests, remove duplicate tests --- .../test_contract_method_abi_decoding.py | 6 ++---- tests/core/utilities/test_abi_named_tree.py | 5 +++-- .../utilities/test_decode_transaction_data.py | 16 ---------------- web3/_utils/contracts.py | 2 +- 4 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 tests/core/utilities/test_decode_transaction_data.py diff --git a/tests/core/contracts/test_contract_method_abi_decoding.py b/tests/core/contracts/test_contract_method_abi_decoding.py index 97efc46d2c..101ffececc 100644 --- a/tests/core/contracts/test_contract_method_abi_decoding.py +++ b/tests/core/contracts/test_contract_method_abi_decoding.py @@ -71,9 +71,8 @@ def test_contract_abi_decoding(web3, abi, data, method, expected): contract = web3.eth.contract(abi=abi) func, params = contract.decode_function_input(data) assert func.fn_name == method - assert params == expected - reinvoke_func = contract.functions[func.fn_name](**params) + reinvoke_func = contract.functions[func.fn_name](*params) rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20}) assert rebuild_txn['data'] == data @@ -98,8 +97,7 @@ def test_contract_abi_encoding_kwargs(web3, abi, method, expected, data): contract = web3.eth.contract(abi=abi) func, params = contract.decode_function_input(data) assert func.fn_name == method - assert params == expected - reinvoke_func = contract.functions[func.fn_name](**params) + reinvoke_func = contract.functions[func.fn_name](*params) rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20}) assert rebuild_txn['data'] == data diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index a7282edca7..64cc6dd721 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -4,7 +4,9 @@ named_arguments_tuple, ) -from .test_abi import TEST_FUNCTION_ABI +from .test_abi import ( + TEST_FUNCTION_ABI, +) abi = TEST_FUNCTION_ABI['inputs'] @@ -20,7 +22,6 @@ def test_named_arguments_decode(): data = named_arguments_tuple(abi, inputs) - s, t, a = data assert data == inputs assert data.s.c[2].y == 10 assert data.t.x == 11 diff --git a/tests/core/utilities/test_decode_transaction_data.py b/tests/core/utilities/test_decode_transaction_data.py deleted file mode 100644 index 5c4e3f56fc..0000000000 --- a/tests/core/utilities/test_decode_transaction_data.py +++ /dev/null @@ -1,16 +0,0 @@ -from web3._utils.contracts import ( - encode_transaction_data, - decode_transaction_data, -) - -from .test_abi import ( - TEST_FUNCTION_ABI, - GET_ABI_INPUTS_OUTPUT, -) - - -def test_decode_transaction_data(): - fn_abi = TEST_FUNCTION_ABI - args = GET_ABI_INPUTS_OUTPUT[1] - data = encode_transaction_data(None, 'f', fn_abi=fn_abi, args=args) - assert decode_transaction_data(TEST_FUNCTION_ABI, data) == args diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 7d89fe9f0f..40a65eda72 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -1,8 +1,8 @@ import functools from eth_abi import ( - encode_abi as eth_abi_encode_abi, decode_abi, + encode_abi as eth_abi_encode_abi, ) from eth_utils import ( add_0x_prefix, From 4641c9ae7858d06bdfb4c7b7525b244a04e867f4 Mon Sep 17 00:00:00 2001 From: banteg Date: Fri, 17 May 2019 08:39:35 +0700 Subject: [PATCH 14/23] Add literal namedtuple constructor --- web3/_utils/abi.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index ae23a2ed12..7ea625a1d4 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -750,8 +750,12 @@ class Tuple(namedtuple('Tuple', fields)): def __new__(self, args): return super().__new__(self, *args) - def __repr__(self): - repr_fmt = '(' + ', '.join(f'{name}=%r' for name in self._fields) + ')' - return repr_fmt % self - return Tuple + + +def Tuple(**kwargs): + """ + Literal namedtuple constructor such that `Tuple(x=1, y=2)` returns `Tuple(x=1, y=2)`. + """ + keys, values = zip(*kwargs.items()) + return foldable_namedtuple(keys)(values) From 4c109c57696342bc97f483d8c3cd6056ad2663df Mon Sep 17 00:00:00 2001 From: banteg Date: Fri, 17 May 2019 10:07:04 +0700 Subject: [PATCH 15/23] Strip leading underscore in namedtuple field names --- web3/_utils/abi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 7ea625a1d4..8d8edd76d8 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -746,6 +746,8 @@ def foldable_namedtuple(fields): """ Customized namedtuple such that `type(x)(x) == x`. """ + fields = [field.lstrip('_') for field in fields] + class Tuple(namedtuple('Tuple', fields)): def __new__(self, args): return super().__new__(self, *args) From 448b1c336975b79f20ccee2832d97cf6dafebbef Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 12:04:35 +0700 Subject: [PATCH 16/23] Fallback to tuple on named fields clash --- web3/_utils/abi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 8d8edd76d8..465c8d9c0a 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -747,6 +747,8 @@ def foldable_namedtuple(fields): Customized namedtuple such that `type(x)(x) == x`. """ fields = [field.lstrip('_') for field in fields] + if '' in fields or len(set(fields)) < len(fields): + return tuple class Tuple(namedtuple('Tuple', fields)): def __new__(self, args): From 5d17e43c53c9fe3fa0ba4648fb93c84db8d5b10c Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 12:18:48 +0700 Subject: [PATCH 17/23] Revert decode_function_input test change --- tests/core/contracts/test_contract_method_abi_decoding.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/core/contracts/test_contract_method_abi_decoding.py b/tests/core/contracts/test_contract_method_abi_decoding.py index 101ffececc..97efc46d2c 100644 --- a/tests/core/contracts/test_contract_method_abi_decoding.py +++ b/tests/core/contracts/test_contract_method_abi_decoding.py @@ -71,8 +71,9 @@ def test_contract_abi_decoding(web3, abi, data, method, expected): contract = web3.eth.contract(abi=abi) func, params = contract.decode_function_input(data) assert func.fn_name == method + assert params == expected - reinvoke_func = contract.functions[func.fn_name](*params) + reinvoke_func = contract.functions[func.fn_name](**params) rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20}) assert rebuild_txn['data'] == data @@ -97,7 +98,8 @@ def test_contract_abi_encoding_kwargs(web3, abi, method, expected, data): contract = web3.eth.contract(abi=abi) func, params = contract.decode_function_input(data) assert func.fn_name == method + assert params == expected - reinvoke_func = contract.functions[func.fn_name](*params) + reinvoke_func = contract.functions[func.fn_name](**params) rebuild_txn = reinvoke_func.buildTransaction({'gas': 0, 'nonce': 0, 'to': '\x00' * 20}) assert rebuild_txn['data'] == data From 0a2f9314bb04ff21383090916a83ed6187af2234 Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 21:00:50 +0700 Subject: [PATCH 18/23] Add ability to convert namedtuple to dict --- web3/_utils/abi.py | 10 ++++++++++ web3/contract.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 465c8d9c0a..eb94d0bf54 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -725,6 +725,13 @@ def named_arguments_tuple(abi, data): return foldable_namedtuple(fields)(decoded) if all(fields) else decoded +def namedtuple_to_dict(data): + def as_dict(item): + return getattr(item, '_asdict', lambda: item)() + + return recursive_map(as_dict, data) + + def named_data_tree(abi, data): abi_type = parse(collapse_if_tuple(abi)) @@ -754,6 +761,9 @@ class Tuple(namedtuple('Tuple', fields)): def __new__(self, args): return super().__new__(self, *args) + def _asdict(self): + return dict(super()._asdict()) + return Tuple diff --git a/web3/contract.py b/web3/contract.py index 2bcc91d215..6068d22ae3 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -37,6 +37,7 @@ map_abi_data, merge_args_and_kwargs, named_arguments_tuple, + namedtuple_to_dict, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, @@ -391,10 +392,12 @@ def callable_check(fn_abi): return get_function_by_identifier(fns, 'selector') @combomethod - def decode_function_input(self, data): + def decode_function_input(self, data, as_dict=True): data = HexBytes(data) func = self.get_function_by_selector(data[:4]) arguments = decode_transaction_data(func.abi, data, normalizers=BASE_RETURN_NORMALIZERS) + if as_dict: + return func, namedtuple_to_dict(arguments) return func, arguments @combomethod From 91c0d36bd5fa22f1de415cc6d1b16df2ffc68493 Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 23:38:34 +0700 Subject: [PATCH 19/23] Don't try to create namedtuple with reserved keywords in fields --- web3/_utils/abi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index eb94d0bf54..a4b2eedc2c 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -6,6 +6,7 @@ import copy import itertools import re +from keyword import kwlist from typing import ( Any, Optional, @@ -754,7 +755,7 @@ def foldable_namedtuple(fields): Customized namedtuple such that `type(x)(x) == x`. """ fields = [field.lstrip('_') for field in fields] - if '' in fields or len(set(fields)) < len(fields): + if '' in fields or len(set(fields)) < len(fields) or set(fields) & set(kwlist): return tuple class Tuple(namedtuple('Tuple', fields)): From a01aa9926eba7534df2bb6fa189016bcd2f87f12 Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 23:39:45 +0700 Subject: [PATCH 20/23] Use dict when parsing argument names --- web3/_utils/abi.py | 41 ++++++++++++++++++++-------------------- web3/_utils/contracts.py | 4 ++-- web3/contract.py | 13 ++++++------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index a4b2eedc2c..ff4b86567a 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -5,8 +5,10 @@ ) import copy import itertools +from keyword import ( + kwlist, +) import re -from keyword import kwlist from typing import ( Any, Optional, @@ -714,42 +716,41 @@ def strip_abi_type(elements): return elements -def named_arguments_tuple(abi, data): +def named_tree(abi, data: tuple): """ - Convert function inputs/outputs tuple to named tuple using names from ABI. - Useful when dealing with structs. The output of this function is accepted where tuples work. - - ABI argument should be fn_abi['inputs'] or fn_abi['outputs'] + Convert function inputs/outputs or event data tuple to dict with names taken from ABI. """ - decoded = [named_data_tree(*item) for item in zip(abi, data)] - fields = [item['name'] for item in abi] - return foldable_namedtuple(fields)(decoded) if all(fields) else decoded - - -def namedtuple_to_dict(data): - def as_dict(item): - return getattr(item, '_asdict', lambda: item)() + names = [item['name'] for item in abi] + items = [named_subtree(*item) for item in zip(abi, data)] + return dict(zip(names, items)) if all(names) else items - return recursive_map(as_dict, data) - -def named_data_tree(abi, data): +def named_subtree(abi, data): abi_type = parse(collapse_if_tuple(abi)) if abi_type.is_array: item_type = abi_type.item_type.to_type_str() item_abi = {**abi, 'type': item_type, 'name': ''} - items = [named_data_tree(item_abi, item) for item in data] + items = [named_subtree(item_abi, item) for item in data] return items if isinstance(abi_type, TupleType): - items = [named_data_tree(*item) for item in zip(abi['components'], data)] names = [item['name'] for item in abi['components']] - return foldable_namedtuple(names)(items) + items = [named_subtree(*item) for item in zip(abi['components'], data)] + return dict(zip(names, items)) return data +def dict_to_namedtuple(data): + def to_tuple(item): + if isinstance(item, dict): + return Tuple(**item) + return item + + return recursive_map(to_tuple, data) + + def foldable_namedtuple(fields): """ Customized namedtuple such that `type(x)(x) == x`. diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 40a65eda72..b14f46d75f 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -31,7 +31,7 @@ get_fallback_func_abi, map_abi_data, merge_args_and_kwargs, - named_arguments_tuple, + named_tree, ) from web3._utils.encoding import ( to_hex, @@ -228,7 +228,7 @@ def decode_transaction_data(fn_abi, data, normalizers=None): decoded = decode_abi(types, data[4:]) if normalizers: decoded = map_abi_data(normalizers, types, decoded) - return named_arguments_tuple(fn_abi['inputs'], decoded) + return named_tree(fn_abi['inputs'], decoded) def get_fallback_function_info(contract_abi=None, fn_abi=None): diff --git a/web3/contract.py b/web3/contract.py index 6068d22ae3..e1fea54ef2 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -29,6 +29,7 @@ from web3._utils.abi import ( abi_to_signature, check_if_arguments_can_be_encoded, + dict_to_namedtuple, fallback_func_abi_exists, filter_by_type, get_abi_output_types, @@ -36,8 +37,7 @@ is_array_type, map_abi_data, merge_args_and_kwargs, - named_arguments_tuple, - namedtuple_to_dict, + named_tree, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, @@ -392,12 +392,10 @@ def callable_check(fn_abi): return get_function_by_identifier(fns, 'selector') @combomethod - def decode_function_input(self, data, as_dict=True): + def decode_function_input(self, data): data = HexBytes(data) func = self.get_function_by_selector(data[:4]) arguments = decode_transaction_data(func.abi, data, normalizers=BASE_RETURN_NORMALIZERS) - if as_dict: - return func, namedtuple_to_dict(arguments) return func, arguments @combomethod @@ -1351,9 +1349,10 @@ def call_contract_function( normalized_data = map_abi_data(_normalizers, output_types, output_data) if decode: - normalized_data = named_arguments_tuple(fn_abi['outputs'], normalized_data) + decoded = named_tree(fn_abi['outputs'], normalized_data) + normalized_data = dict_to_namedtuple(decoded) - if len(normalized_data) == 1: + if isinstance(normalized_data, list) and len(normalized_data) == 1: return normalized_data[0] else: return normalized_data From 62d11c8e32c328b507168ba7fb643cdc352f8115 Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 23:47:10 +0700 Subject: [PATCH 21/23] Add tuple support and decoding to events --- web3/_utils/events.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web3/_utils/events.py b/web3/_utils/events.py index 81defc25e0..9d1e8bcef8 100644 --- a/web3/_utils/events.py +++ b/web3/_utils/events.py @@ -22,6 +22,9 @@ to_hex, to_tuple, ) +from eth_utils.abi import ( + collapse_if_tuple, +) from eth_utils.toolz import ( complement, compose, @@ -54,6 +57,7 @@ get_abi_input_names, get_indexed_event_inputs, map_abi_data, + named_tree, normalize_event_input_types, ) @@ -152,7 +156,7 @@ def get_event_abi_types_for_decoding(event_inputs): if input_abi['indexed'] and is_dynamic_sized_type(input_abi['type']): yield 'bytes32' else: - yield input_abi['type'] + yield collapse_if_tuple(input_abi) @curry @@ -202,6 +206,10 @@ def get_event_data(event_abi, log_entry): log_data_types, decoded_log_data ) + named_log_data = named_tree( + log_data_normalized_inputs, + normalized_log_data, + ) decoded_topic_data = [ decode_single(topic_type, topic_data) @@ -216,7 +224,7 @@ def get_event_data(event_abi, log_entry): event_args = dict(itertools.chain( zip(log_topic_names, normalized_topic_data), - zip(log_data_names, normalized_log_data), + named_log_data.items(), )) event_data = { From 37d7c32ff955c7262bf42aada54b23e7a670d0b3 Mon Sep 17 00:00:00 2001 From: banteg Date: Sat, 18 May 2019 23:52:04 +0700 Subject: [PATCH 22/23] Update named_tree tests --- tests/core/utilities/test_abi_named_tree.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index 64cc6dd721..729e41fa43 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -1,7 +1,8 @@ from web3._utils.abi import ( check_if_arguments_can_be_encoded, + dict_to_namedtuple, foldable_namedtuple, - named_arguments_tuple, + named_tree, ) from .test_abi import ( @@ -21,7 +22,8 @@ def test_named_arguments_decode(): - data = named_arguments_tuple(abi, inputs) + decoded = named_tree(abi, inputs) + data = dict_to_namedtuple(decoded) assert data == inputs assert data.s.c[2].y == 10 assert data.t.x == 11 @@ -29,11 +31,13 @@ def test_named_arguments_decode(): def test_namedtuples_encodable(): - args = named_arguments_tuple(abi, inputs) + kwargs = named_tree(abi, inputs) + args = dict_to_namedtuple(kwargs) assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, args, {}) + assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, (), kwargs) def test_foldable_namedtuple(): - item = foldable_namedtuple('a b c')([1, 2, 3]) + item = foldable_namedtuple(['a', 'b', 'c'])([1, 2, 3]) assert type(item)(item) == item == (1, 2, 3) assert item.c == 3 From e328c5b9d9b974847714b8c39526fa96a119add7 Mon Sep 17 00:00:00 2001 From: banteg Date: Tue, 9 Jul 2019 19:20:43 +0700 Subject: [PATCH 23/23] fix vyper-specific scalar as struct output --- web3/contract.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web3/contract.py b/web3/contract.py index e1fea54ef2..da1ab80ac5 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -1,6 +1,9 @@ """Interaction with smart contracts over Web3 connector. """ +from collections.abc import ( + Sequence, +) import copy import itertools @@ -1352,7 +1355,7 @@ def call_contract_function( decoded = named_tree(fn_abi['outputs'], normalized_data) normalized_data = dict_to_namedtuple(decoded) - if isinstance(normalized_data, list) and len(normalized_data) == 1: + if isinstance(normalized_data, Sequence) and len(normalized_data) == 1: return normalized_data[0] else: return normalized_data