From feb002bbadc596dffc728a0bca63fa0814572101 Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 26 Jan 2023 17:14:59 -0700 Subject: [PATCH 01/35] tuple contract tests and fixtures added --- .../contracts/test_contract_call_interface.py | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index 4236d0766d..c36e7f18d5 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -60,6 +60,32 @@ def nested_tuple_contract(w3, address_conversion_func): return deploy(w3, nested_tuple_contract_factory, address_conversion_func) +TUPLE_CONTRACT_DATA_DECODE_TUPLES = { + **TUPLE_CONTRACT_DATA, + "decode_tuples": True, +} + + +NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES = { + **NESTED_TUPLE_CONTRACT_DATA, + "decode_tuples": True, +} + + +@pytest.fixture +def tuple_contract_with_decode_tuples(w3, address_conversion_func): + tuple_contract_factory = w3.eth.contract(**TUPLE_CONTRACT_DATA_DECODE_TUPLES) + return deploy(w3, tuple_contract_factory, address_conversion_func) + + +@pytest.fixture +def nested_tuple_contract_with_decode_tuples(w3, address_conversion_func): + nested_tuple_contract_factory = w3.eth.contract( + **NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES + ) + return deploy(w3, nested_tuple_contract_factory, address_conversion_func) + + @pytest.fixture(params=[b"\x04\x06", "0x0406"]) def bytes_contract(w3, request, address_conversion_func): bytes_contract_factory = w3.eth.contract(**BYTES_CONTRACT_DATA) @@ -902,6 +928,115 @@ def test_call_tuple_contract(tuple_contract, method_input, expected): assert result == expected +@pytest.mark.parametrize( + "method_input, expected", + ( + ( + { + "a": 123, + "b": [1, 2], + "c": [ + { + "x": 234, + "y": [True, False], + "z": [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + }, + { + "x": 345, + "y": [False, False], + "z": [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + }, + ], + }, + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ), + ( + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ), + ), +) +def test_call_tuple_contract_with_decode_tuples_set( + tuple_contract_with_decode_tuples, method_input, expected +): + result = tuple_contract_with_decode_tuples.functions.method(method_input).call() + assert result == expected + assert result._fields == ("a", "b", "c") + + @pytest.mark.parametrize( "method_input, expected", ( @@ -988,6 +1123,97 @@ def test_call_nested_tuple_contract(nested_tuple_contract, method_input, expecte assert result == expected +@pytest.mark.parametrize( + "method_input, expected", + ( + ( + { + "t": [ + { + "u": [ + {"x": 1, "y": 2}, + {"x": 3, "y": 4}, + {"x": 5, "y": 6}, + ] + }, + { + "u": [ + {"x": 7, "y": 8}, + {"x": 9, "y": 10}, + {"x": 11, "y": 12}, + ] + }, + ] + }, + ( + [ + ( + [ + (1, 2), + (3, 4), + (5, 6), + ], + ), + ( + [ + (7, 8), + (9, 10), + (11, 12), + ], + ), + ], + ), + ), + ( + ( + [ + ( + [ + (1, 2), + (3, 4), + (5, 6), + ], + ), + ( + [ + (7, 8), + (9, 10), + (11, 12), + ], + ), + ], + ), + ( + [ + ( + [ + (1, 2), + (3, 4), + (5, 6), + ], + ), + ( + [ + (7, 8), + (9, 10), + (11, 12), + ], + ), + ], + ), + ), + ), +) +def test_call_nested_tuple_contract_with_decode_tuples_set( + nested_tuple_contract_with_decode_tuples, method_input, expected +): + result = nested_tuple_contract_with_decode_tuples.functions.method( + method_input + ).call() + assert result == expected + assert result._fields == ("t",) + + def test_call_revert_contract(revert_contract): with pytest.raises(TransactionFailed, match="Function has been reverted."): # eth-tester will do a gas estimation if we don't submit a gas value, @@ -1031,6 +1257,28 @@ async def async_nested_tuple_contract(async_w3, address_conversion_func): ) +@pytest_asyncio.fixture +async def async_tuple_contract_with_decode_tuples(async_w3, address_conversion_func): + async_tuple_contract_factory = async_w3.eth.contract( + **TUPLE_CONTRACT_DATA_DECODE_TUPLES + ) + return await async_deploy( + async_w3, async_tuple_contract_factory, address_conversion_func + ) + + +@pytest_asyncio.fixture +async def async_nested_tuple_contract_with_decode_tuples( + async_w3, address_conversion_func +): + async_nested_tuple_contract_factory = async_w3.eth.contract( + **NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES + ) + return await async_deploy( + async_w3, async_nested_tuple_contract_factory, address_conversion_func + ) + + @pytest.fixture def async_bytes_contract_factory(async_w3): return async_w3.eth.contract(**BYTES_CONTRACT_DATA) @@ -1887,6 +2135,118 @@ async def test_async_call_tuple_contract(async_tuple_contract, method_input, exp assert result == expected +@pytest.mark.asyncio +@pytest.mark.parametrize( + "method_input, expected", + ( + ( + { + "a": 123, + "b": [1, 2], + "c": [ + { + "x": 234, + "y": [True, False], + "z": [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + }, + { + "x": 345, + "y": [False, False], + "z": [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + }, + ], + }, + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ), + ( + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ), + ), +) +async def test_async_call_tuple_contract_with_decode_tuples_set( + async_tuple_contract_with_decode_tuples, method_input, expected +): + result = await async_tuple_contract_with_decode_tuples.functions.method( + method_input + ).call() + assert result == expected + assert result._fields == ("a", "b", "c") + + @pytest.mark.asyncio @pytest.mark.parametrize( "method_input, expected", @@ -1976,6 +2336,98 @@ async def test_async_call_nested_tuple_contract( assert result == expected +@pytest.mark.asyncio +@pytest.mark.parametrize( + "method_input, expected", + ( + ( + { + "t": [ + { + "u": [ + {"x": 1, "y": 2}, + {"x": 3, "y": 4}, + {"x": 5, "y": 6}, + ] + }, + { + "u": [ + {"x": 7, "y": 8}, + {"x": 9, "y": 10}, + {"x": 11, "y": 12}, + ] + }, + ] + }, + ( + [ + ( + [ + (1, 2), + (3, 4), + (5, 6), + ], + ), + ( + [ + (7, 8), + (9, 10), + (11, 12), + ], + ), + ], + ), + ), + ( + ( + [ + ( + [ + (1, 2), + (3, 4), + (5, 6), + ], + ), + ( + [ + (7, 8), + (9, 10), + (11, 12), + ], + ), + ], + ), + ( + [ + ( + [ + (1, 2), + (3, 4), + (5, 6), + ], + ), + ( + [ + (7, 8), + (9, 10), + (11, 12), + ], + ), + ], + ), + ), + ), +) +async def test_async_call_nested_tuple_contract_with_decode_tuples_set( + async_nested_tuple_contract_with_decode_tuples, method_input, expected +): + result = await async_nested_tuple_contract_with_decode_tuples.functions.method( + method_input + ).call() + assert result == expected + assert result._fields == ("t",) + + @pytest.mark.asyncio async def test_async_call_revert_contract(async_revert_contract): with pytest.raises(TransactionFailed, match="Function has been reverted."): From 63c118402cbebda29b2148a44612c7fc2f22d0e0 Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 26 Jan 2023 18:27:44 -0700 Subject: [PATCH 02/35] add abi utils and tests, start typing --- tests/core/utilities/test_abi_named_tree.py | 49 +++++++++++++++ web3/_utils/abi.py | 67 +++++++++++++++++++++ 2 files changed, 116 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..abddc1b9eb --- /dev/null +++ b/tests/core/utilities/test_abi_named_tree.py @@ -0,0 +1,49 @@ +from eth_abi.codec import ( + ABICodec, +) +from eth_abi.registry import ( + registry as default_registry, +) + +from web3._utils.abi import ( + check_if_arguments_can_be_encoded, + dict_to_namedtuple, + foldable_namedtuple, + named_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 + [[(14, 15), (16, 17)], [(18, 19)]], # Value for b +) + + +def test_named_arguments_decode(): + 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 + assert data.a == 13 + + +def test_namedtuples_encodable(): + registry = default_registry.copy() + codec = ABICodec(registry) + kwargs = named_tree(abi, inputs) + args = dict_to_namedtuple(kwargs) + assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, (), kwargs) + assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, args, {}) + + +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 9a39b6e8d6..3aa4e30016 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -906,3 +906,70 @@ def build_strict_registry() -> ABIRegistry: label="string", ) return registry + + +def named_tree(abi: List[Dict[str, Any]], data: Tuple[Any, ...]) -> Dict[str, Any]: + """ + Convert function inputs/outputs or event data tuple to dict with names from ABI. + """ + names = [item["name"] for item in abi] + items = [named_subtree(*item) for item in zip(abi, data)] + + # TODO how to handle if names and items end up different len + # return dict(zip(names, items)) if all(names) else items + return dict(zip(names, items)) + + +def named_subtree( + abi: Dict[str, Any], data: Tuple[Any, ...] +) -> Union[TupleType, Dict[str, Any], Tuple[Any, ...]]: + 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_subtree(item_abi, item) for item in data] + return items + + if isinstance(abi_type, TupleType): + names = [item["name"] for item in abi["components"]] + items = [named_subtree(*item) for item in zip(abi["components"], data)] + return dict(zip(names, items)) + + return data + + +def dict_to_namedtuple(data: Dict[str, Any]) -> TupleType: + def to_tuple(item): + if isinstance(item, dict): + return generate_namedtuple_from_dict(**item) + return item + + breakpoint() + return recursive_map(to_tuple, data) + + +def foldable_namedtuple(fields): + """ + Customized namedtuple such that `type(x)(x) == x`. + """ + + class ABIDecodedNamedTuple(namedtuple("ABIDecodedNamedTuple", fields, rename=True)): + def __new__(self, args): + return super().__new__(self, *args) + + def _asdict(self): + return dict(super()._asdict()) + + return ABIDecodedNamedTuple + + +def generate_namedtuple_from_dict(**kwargs): + """ + Literal namedtuple constructor such that: + `generate_namedtuple_from_dict(x=1, y=2)` + returns `generate_namedtuple_from_dict(x=1, y=2)`. + """ + keys, values = zip(*kwargs.items()) + + return foldable_namedtuple(keys)(values) From 86bd159a570597c836bea4303f7c179c96088695 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 27 Jan 2023 11:19:08 -0700 Subject: [PATCH 03/35] finished typing --- web3/_utils/abi.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 3aa4e30016..312fdae023 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -922,7 +922,7 @@ def named_tree(abi: List[Dict[str, Any]], data: Tuple[Any, ...]) -> Dict[str, An def named_subtree( abi: Dict[str, Any], data: Tuple[Any, ...] -) -> Union[TupleType, Dict[str, Any], Tuple[Any, ...]]: +) -> Union[Dict[str, Any], Tuple[Any, ...], Any]: abi_type = parse(collapse_if_tuple(abi)) if abi_type.is_array: @@ -939,32 +939,33 @@ def named_subtree( return data -def dict_to_namedtuple(data: Dict[str, Any]) -> TupleType: - def to_tuple(item): +def dict_to_namedtuple(data: Dict[str, Any]) -> Tuple[Any, ...]: + def to_tuple( + item: Union[Dict[str, Any], List[Any]] + ) -> Union[Tuple[Any, ...], List[Any]]: if isinstance(item, dict): return generate_namedtuple_from_dict(**item) return item - breakpoint() return recursive_map(to_tuple, data) -def foldable_namedtuple(fields): +def foldable_namedtuple(fields: Tuple[Any, ...]) -> Tuple[Any, ...]: """ Customized namedtuple such that `type(x)(x) == x`. """ - class ABIDecodedNamedTuple(namedtuple("ABIDecodedNamedTuple", fields, rename=True)): - def __new__(self, args): + class ABIDecodedNamedTuple(namedtuple("ABIDecodedNamedTuple", fields, rename=True)): # type: ignore # noqa: E501 + def __new__(self, args: Any) -> "ABIDecodedNamedTuple": return super().__new__(self, *args) - def _asdict(self): + def _asdict(self) -> Dict[str, Any]: return dict(super()._asdict()) - return ABIDecodedNamedTuple + return ABIDecodedNamedTuple # type: ignore -def generate_namedtuple_from_dict(**kwargs): +def generate_namedtuple_from_dict(**kwargs: Dict[str, Any]) -> Tuple[Any, ...]: """ Literal namedtuple constructor such that: `generate_namedtuple_from_dict(x=1, y=2)` @@ -972,4 +973,4 @@ def generate_namedtuple_from_dict(**kwargs): """ keys, values = zip(*kwargs.items()) - return foldable_namedtuple(keys)(values) + return foldable_namedtuple(keys)(values) # type: ignore From bd43a73e6d6fb5a83798dd97dbc27271d0df16bb Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 27 Jan 2023 11:22:04 -0700 Subject: [PATCH 04/35] add _utils/contracts.py --- web3/_utils/contracts.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 61ee9315fb..a80d4ab7ea 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -10,6 +10,10 @@ cast, ) +from eth_abi import ( + decode, +) + from eth_abi.codec import ( ABICodec, ) @@ -47,6 +51,7 @@ get_receive_func_abi, map_abi_data, merge_args_and_kwargs, + named_tree, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, @@ -312,6 +317,15 @@ def encode_transaction_data( return add_0x_prefix(encode_abi(w3, fn_abi, fn_arguments, fn_selector)) +def decode_transaction_data(fn_abi, data, normalizers=None): + data = HexBytes(data) + types = get_abi_input_types(fn_abi) + decoded = decode(types, data[4:]) + if normalizers: + decoded = map_abi_data(normalizers, types, decoded) + return named_tree(fn_abi["inputs"], decoded) + + def get_fallback_function_info( contract_abi: Optional[ABI] = None, fn_abi: Optional[ABIFunction] = None ) -> Tuple[ABIFunction, HexStr, Tuple[Any, ...]]: From 76431357b2ca2bf800580e708dc56b47386bfd26 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 27 Jan 2023 11:36:45 -0700 Subject: [PATCH 05/35] add decode_tuples flag to base_contract --- web3/contract/base_contract.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 89c9622a43..457b136622 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -52,6 +52,7 @@ receive_func_abi_exists, ) from web3._utils.contracts import ( + decode_transaction_data, encode_abi, find_matching_event_abi, find_matching_fn_abi, @@ -159,6 +160,7 @@ class BaseContract: bytecode_runtime = None clone_bin = None + decode_tuples = None dev_doc = None interface = None metadata = None @@ -254,16 +256,12 @@ def decode_function_input( ) -> Tuple["BaseContractFunction", Dict[str, Any]]: # type ignored b/c expects data arg to be HexBytes data = HexBytes(data) # type: ignore - selector, params = data[:4], data[4:] - func = self.get_function_by_selector(selector) - - names = get_abi_input_names(func.abi) - types = get_abi_input_types(func.abi) - - decoded = self.w3.codec.decode(types, cast(HexBytes, params)) - normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) + func = self.get_function_by_selector(data[:4]) + arguments = decode_transaction_data( + func.abi, data, normalizers=BASE_RETURN_NORMALIZERS + ) + return func, arguments - return func, dict(zip(names, normalized)) @combomethod def find_functions_by_args(self, *args: Any) -> "BaseContractFunction": @@ -435,6 +433,7 @@ def __init__( w3: "Web3", contract_function_class: Type["BaseContractFunction"], address: Optional[ChecksumAddress] = None, + decode_tuples: Optional[bool] = None, ) -> None: self.abi = abi self.w3 = w3 @@ -451,6 +450,7 @@ def __init__( w3=self.w3, contract_abi=self.abi, address=self.address, + decode_tuples=decode_tuples, function_identifier=func["name"], ), ) @@ -668,6 +668,7 @@ class BaseContractFunction: abi: ABIFunction = None transaction: TxParams = None arguments: Tuple[Any, ...] = None + decode_tuples: Optional[bool] = None args: Any = None kwargs: Any = None @@ -1086,11 +1087,13 @@ def __init__( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = None, ) -> None: self.w3 = w3 self.address = address self.abi = abi self._functions = None + self.decode_tuples = decode_tuples if self.abi: if transaction is None: @@ -1103,6 +1106,7 @@ def __init__( w3=self.w3, contract_abi=self.abi, address=self.address, + decode_tuples=self.decode_tuples, function_identifier=func["name"], ) From 528ee012662b4f739a66db04ccd8649f8e63b02c Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 27 Jan 2023 11:53:33 -0700 Subject: [PATCH 06/35] all contract changes added --- web3/_utils/contracts.py | 1 - web3/contract/async_contract.py | 14 +++++++++++--- web3/contract/base_contract.py | 4 ---- web3/contract/contract.py | 14 +++++++++++--- web3/contract/utils.py | 12 ++++++++++++ 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index a80d4ab7ea..81fe07060a 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -13,7 +13,6 @@ from eth_abi import ( decode, ) - from eth_abi.codec import ( ABICodec, ) diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index d71d21022e..49b7d7ca0b 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -79,8 +79,9 @@ def __init__( abi: ABI, w3: "Web3", address: Optional[ChecksumAddress] = None, + decode_tuples: Optional[bool] = None, ) -> None: - super().__init__(abi, w3, AsyncContractFunction, address) + super().__init__(abi, w3, AsyncContractFunction, address, decode_tuples) class AsyncContractEvents(BaseContractEvents): @@ -115,7 +116,9 @@ def __init__(self, address: Optional[ChecksumAddress] = None) -> None: raise TypeError( "The address argument is required to instantiate a contract." ) - self.functions = AsyncContractFunctions(self.abi, self.w3, self.address) + self.functions = AsyncContractFunctions( + self.abi, self.w3, self.address, self.decode_tuples + ) self.caller = AsyncContractCaller(self.abi, self.w3, self.address) self.events = AsyncContractEvents(self.abi, self.w3, self.address) self.fallback = AsyncContract.get_fallback_function( @@ -147,7 +150,9 @@ def factory( normalizers=normalizers, ), ) - contract.functions = AsyncContractFunctions(contract.abi, contract.w3) + contract.functions = AsyncContractFunctions( + contract.abi, contract.w3, contract.decode_tuples + ) contract.caller = AsyncContractCaller( contract.abi, contract.w3, contract.address ) @@ -289,6 +294,7 @@ async def call( self.abi, state_override, ccip_read_enabled, + self.decode_tuples, *self.args, **self.kwargs, ) @@ -468,6 +474,7 @@ def __init__( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = None, ) -> None: super().__init__( abi=abi, @@ -477,6 +484,7 @@ def __init__( block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, contract_function_class=AsyncContractFunction, + decode_tuples=decode_tuples, ) def __call__( diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 457b136622..add7b02843 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -43,11 +43,8 @@ check_if_arguments_can_be_encoded, fallback_func_abi_exists, filter_by_type, - get_abi_input_names, - get_abi_input_types, get_constructor_abi, is_array_type, - map_abi_data, merge_args_and_kwargs, receive_func_abi_exists, ) @@ -262,7 +259,6 @@ def decode_function_input( ) return func, arguments - @combomethod def find_functions_by_args(self, *args: Any) -> "BaseContractFunction": def callable_check(fn_abi: ABIFunction) -> bool: diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 0e64885a3d..845b389280 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -79,8 +79,9 @@ def __init__( abi: ABI, w3: "Web3", address: Optional[ChecksumAddress] = None, + decode_tuples: Optional[bool] = None, ) -> None: - super().__init__(abi, w3, ContractFunction, address) + super().__init__(abi, w3, ContractFunction, address, decode_tuples) class ContractEvents(BaseContractEvents): @@ -231,7 +232,9 @@ def __init__(self, address: Optional[ChecksumAddress] = None) -> None: "The address argument is required to instantiate a contract." ) - self.functions = ContractFunctions(self.abi, _w3, self.address) + self.functions = ContractFunctions( + self.abi, _w3, self.address, self.decode_tuples + ) self.caller = ContractCaller(self.abi, _w3, self.address) self.events = ContractEvents(self.abi, _w3, self.address) self.fallback = Contract.get_fallback_function( @@ -269,7 +272,9 @@ def factory( normalizers=normalizers, ), ) - contract.functions = ContractFunctions(contract.abi, contract.w3) + contract.functions = ContractFunctions( + contract.abi, contract.w3, contract.decode_tuples + ) contract.caller = ContractCaller(contract.abi, contract.w3, contract.address) contract.events = ContractEvents(contract.abi, contract.w3) contract.fallback = Contract.get_fallback_function( @@ -404,6 +409,7 @@ def call( self.abi, state_override, ccip_read_enabled, + self.decode_tuples, *self.args, **self.kwargs, ) @@ -462,6 +468,7 @@ def __init__( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = None, ) -> None: super().__init__( abi=abi, @@ -471,6 +478,7 @@ def __init__( block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, contract_function_class=ContractFunction, + decode_tuples=decode_tuples, ) def __call__( diff --git a/web3/contract/utils.py b/web3/contract/utils.py index afe4bc7307..8db67b457b 100644 --- a/web3/contract/utils.py +++ b/web3/contract/utils.py @@ -21,9 +21,11 @@ ) from web3._utils.abi import ( + dict_to_namedtuple, filter_by_type, get_abi_output_types, map_abi_data, + named_tree, ) from web3._utils.async_transactions import ( fill_transaction_defaults as async_fill_transaction_defaults, @@ -70,6 +72,7 @@ def call_contract_function( fn_abi: Optional[ABIFunction] = None, state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = None, *args: Any, **kwargs: Any, ) -> Any: @@ -129,6 +132,10 @@ def call_contract_function( ) normalized_data = map_abi_data(_normalizers, output_types, output_data) + if decode_tuples: + decoded = named_tree(fn_abi["outputs"], normalized_data) + normalized_data = dict_to_namedtuple(decoded) + if len(normalized_data) == 1: return normalized_data[0] else: @@ -273,6 +280,7 @@ async def async_call_contract_function( fn_abi: Optional[ABIFunction] = None, state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = None, *args: Any, **kwargs: Any, ) -> Any: @@ -332,6 +340,10 @@ async def async_call_contract_function( ) normalized_data = map_abi_data(_normalizers, output_types, output_data) + if decode_tuples: + decoded = named_tree(fn_abi["outputs"], normalized_data) + normalized_data = dict_to_namedtuple(decoded) + if len(normalized_data) == 1: return normalized_data[0] else: From 053ae6e9b03a2bea63c3d08e76d59531be5ff755 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 27 Jan 2023 15:13:08 -0700 Subject: [PATCH 07/35] remove change to decode_function_input to a separate pr --- web3/_utils/abi.py | 5 ++++- web3/contract/base_contract.py | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 312fdae023..929c2b48d2 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -908,7 +908,10 @@ def build_strict_registry() -> ABIRegistry: return registry -def named_tree(abi: List[Dict[str, Any]], data: Tuple[Any, ...]) -> Dict[str, Any]: +# def named_tree(abi: List[Dict[str, Any]], data: Tuple[Any, ...]) -> Dict[str, Any]: +def named_tree( + abi: Sequence[ABIFunctionParams], data: Tuple[Any, ...] +) -> Dict[str, Any]: """ Convert function inputs/outputs or event data tuple to dict with names from ABI. """ diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index add7b02843..ed2f379b5b 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -43,13 +43,15 @@ check_if_arguments_can_be_encoded, fallback_func_abi_exists, filter_by_type, + get_abi_input_names, + get_abi_input_types, get_constructor_abi, is_array_type, + map_abi_data, merge_args_and_kwargs, receive_func_abi_exists, ) from web3._utils.contracts import ( - decode_transaction_data, encode_abi, find_matching_event_abi, find_matching_fn_abi, @@ -253,11 +255,16 @@ def decode_function_input( ) -> Tuple["BaseContractFunction", Dict[str, Any]]: # type ignored b/c expects data arg to be HexBytes data = HexBytes(data) # type: ignore - func = self.get_function_by_selector(data[:4]) - arguments = decode_transaction_data( - func.abi, data, normalizers=BASE_RETURN_NORMALIZERS - ) - return func, arguments + selector, params = data[:4], data[4:] + func = self.get_function_by_selector(selector) + + names = get_abi_input_names(func.abi) + types = get_abi_input_types(func.abi) + + decoded = self.w3.codec.decode(types, cast(HexBytes, params)) + normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) + + return func, dict(zip(names, normalized)) @combomethod def find_functions_by_args(self, *args: Any) -> "BaseContractFunction": From 9e94690288ec36410f5c8743dc1c4e9c3076cd47 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 27 Jan 2023 15:51:11 -0700 Subject: [PATCH 08/35] work on typing --- web3/_utils/abi.py | 8 ++++++-- web3/_utils/contracts.py | 13 ------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 929c2b48d2..452f0d34f2 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -924,13 +924,17 @@ def named_tree( def named_subtree( - abi: Dict[str, Any], data: Tuple[Any, ...] + # TODO make this a better type + abi: Any, + data: Tuple[Any, ...], ) -> Union[Dict[str, Any], Tuple[Any, ...], Any]: - abi_type = parse(collapse_if_tuple(abi)) + abi_type = parse(collapse_if_tuple(dict(abi))) if abi_type.is_array: item_type = abi_type.item_type.to_type_str() item_abi = {**abi, "type": item_type, "name": ""} + # cast(ABIFunctionParams, item_abi) + # breakpoint() items = [named_subtree(item_abi, item) for item in data] return items diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 81fe07060a..61ee9315fb 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -10,9 +10,6 @@ cast, ) -from eth_abi import ( - decode, -) from eth_abi.codec import ( ABICodec, ) @@ -50,7 +47,6 @@ get_receive_func_abi, map_abi_data, merge_args_and_kwargs, - named_tree, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, @@ -316,15 +312,6 @@ def encode_transaction_data( return add_0x_prefix(encode_abi(w3, fn_abi, fn_arguments, fn_selector)) -def decode_transaction_data(fn_abi, data, normalizers=None): - data = HexBytes(data) - types = get_abi_input_types(fn_abi) - decoded = decode(types, data[4:]) - if normalizers: - decoded = map_abi_data(normalizers, types, decoded) - return named_tree(fn_abi["inputs"], decoded) - - def get_fallback_function_info( contract_abi: Optional[ABI] = None, fn_abi: Optional[ABIFunction] = None ) -> Tuple[ABIFunction, HexStr, Tuple[Any, ...]]: From 6a0fafa392528c6e73fab0cc1637694ea78c8000 Mon Sep 17 00:00:00 2001 From: pacrob Date: Tue, 31 Jan 2023 07:33:37 -0700 Subject: [PATCH 09/35] wip --- tests/core/utilities/test_abi_named_tree.py | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index abddc1b9eb..4c6e6b1547 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -24,6 +24,43 @@ [[(14, 15), (16, 17)], [(18, 19)]], # Value for b ) +sample_abi_inputs = { + "inputs": [ + { + "components": [ + {"name": "a", "type": "uint256"}, + {"name": "b", "type": "uint256[]"}, + { + "components": [ + {"name": "x", "type": "uint256"}, + {"name": "y", "type": "uint256"}, + ], + "name": "c", + "type": "tuple[]", + }, + ], + "name": "s", + "type": "tuple", + }, + { + "components": [ + {"name": "x", "type": "uint256"}, + {"name": "y", "type": "uint256"}, + ], + "name": "t", + "type": "tuple", + }, + {"name": "a", "type": "uint256"}, + { + "components": [ + {"name": "x", "type": "uint256"}, + {"name": "y", "type": "uint256"}, + ], + "name": "b", + "type": "tuple[][]", + }, + ] +} def test_named_arguments_decode(): decoded = named_tree(abi, inputs) From fa49d6cab4961e8999aaf40b3e43177ef4481add Mon Sep 17 00:00:00 2001 From: pacrob Date: Tue, 31 Jan 2023 13:31:55 -0700 Subject: [PATCH 10/35] catch mismatched abi data and test for namedtuple rename --- tests/core/utilities/test_abi_named_tree.py | 102 ++++++++++++-------- web3/_utils/abi.py | 13 ++- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index 4c6e6b1547..e85952d671 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -1,3 +1,5 @@ +import pytest + from eth_abi.codec import ( ABICodec, ) @@ -11,70 +13,86 @@ foldable_namedtuple, named_tree, ) +from web3.exceptions import ( + MismatchedABI, +) from .test_abi import ( TEST_FUNCTION_ABI, ) -abi = TEST_FUNCTION_ABI["inputs"] -inputs = ( +full_abi_inputs = TEST_FUNCTION_ABI["inputs"] +full_values = ( (1, [2, 3, 4], [(5, 6), (7, 8), (9, 10)]), # Value for s (11, 12), # Value for t 13, # Value for a [[(14, 15), (16, 17)], [(18, 19)]], # Value for b ) -sample_abi_inputs = { - "inputs": [ - { - "components": [ - {"name": "a", "type": "uint256"}, - {"name": "b", "type": "uint256[]"}, - { - "components": [ - {"name": "x", "type": "uint256"}, - {"name": "y", "type": "uint256"}, - ], - "name": "c", - "type": "tuple[]", - }, - ], - "name": "s", - "type": "tuple", - }, - { - "components": [ - {"name": "x", "type": "uint256"}, - {"name": "y", "type": "uint256"}, - ], - "name": "t", - "type": "tuple", - }, - {"name": "a", "type": "uint256"}, - { - "components": [ - {"name": "x", "type": "uint256"}, - {"name": "y", "type": "uint256"}, - ], - "name": "b", - "type": "tuple[][]", - }, - ] -} def test_named_arguments_decode(): - decoded = named_tree(abi, inputs) + decoded = named_tree(full_abi_inputs, full_values) data = dict_to_namedtuple(decoded) - assert data == inputs + assert data == full_values assert data.s.c[2].y == 10 assert data.t.x == 11 assert data.a == 13 +short_abi_inputs_with_disallowed_names = [ + { + "components": [ + {"name": "from", "type": "uint256"}, + {"name": "to", "type": "uint256[]"}, + { + "components": [ + {"name": "_x", "type": "uint256"}, + {"name": "_y", "type": "uint256"}, + ], + "name": "c", + "type": "tuple[]", + }, + ], + "name": "s", + "type": "tuple", + }, +] + +short_values = ((1, [2, 3, 4], [(5, 6), (7, 8), (9, 10)]),) + + +def test_named_arguments_decode_rename(): + decoded = named_tree(short_abi_inputs_with_disallowed_names, short_values) + data = dict_to_namedtuple(decoded) + assert data == short_values + assert data._fields == ("s",) + + # python keyword "from" renamed to "_0" + assert data.s._fields == ("_0", "to", "c") + + # field names starting with "_" - "_x" and "_y" - renamed to "_0" and "_1" + assert data.s.c[0]._fields == ("_0", "_1") + assert data.s.c[2]._1 == 10 + assert data.s.to[1] == 3 + + +@pytest.mark.parametrize( + "values", + ( + ((1, [2, 3, 4], [(5,), (7, 8), (9, 10)]),), + ((1, [2, 3, 4], [(5, 6, 11), (7, 8), (9, 10)]),), + ((1, [(5, 6), (7, 8), (9, 10)]),), + ), +) +def test_named_arguments_decode_with_misshapen_inputs(values): + with pytest.raises(MismatchedABI): + named_tree(short_abi_inputs_with_disallowed_names, values) + + def test_namedtuples_encodable(): registry = default_registry.copy() codec = ABICodec(registry) - kwargs = named_tree(abi, inputs) + kwargs = named_tree(full_abi_inputs, full_values) args = dict_to_namedtuple(kwargs) assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, (), kwargs) assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, args, {}) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 452f0d34f2..97fc19de7f 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -74,6 +74,7 @@ ) from web3.exceptions import ( FallbackNotFound, + MismatchedABI, ) from web3.types import ( ABI, @@ -920,6 +921,7 @@ def named_tree( # TODO how to handle if names and items end up different len # return dict(zip(names, items)) if all(names) else items + return dict(zip(names, items)) @@ -933,15 +935,20 @@ def named_subtree( if abi_type.is_array: item_type = abi_type.item_type.to_type_str() item_abi = {**abi, "type": item_type, "name": ""} - # cast(ABIFunctionParams, item_abi) - # breakpoint() items = [named_subtree(item_abi, item) for item in data] return items if isinstance(abi_type, TupleType): names = [item["name"] for item in abi["components"]] items = [named_subtree(*item) for item in zip(abi["components"], data)] - return dict(zip(names, items)) + + if len(names) == len(data): + return dict(zip(names, items)) + else: + raise MismatchedABI( + f"ABI fields {names} has length {len(names)} but received " + f"data {data} with length {len(data)}" + ) return data From 8b09a3fb06335b0a8a4aeb6cacb39121a96a7a77 Mon Sep 17 00:00:00 2001 From: pacrob Date: Tue, 31 Jan 2023 14:55:37 -0700 Subject: [PATCH 11/35] remove todo note --- web3/_utils/abi.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 97fc19de7f..2d563affb2 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -909,7 +909,6 @@ def build_strict_registry() -> ABIRegistry: return registry -# def named_tree(abi: List[Dict[str, Any]], data: Tuple[Any, ...]) -> Dict[str, Any]: def named_tree( abi: Sequence[ABIFunctionParams], data: Tuple[Any, ...] ) -> Dict[str, Any]: @@ -919,9 +918,6 @@ def named_tree( names = [item["name"] for item in abi] items = [named_subtree(*item) for item in zip(abi, data)] - # TODO how to handle if names and items end up different len - # return dict(zip(names, items)) if all(names) else items - return dict(zip(names, items)) From 2b735fc360da9824f892d7f6e89fae9c18675dde Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 1 Feb 2023 13:05:18 -0700 Subject: [PATCH 12/35] first draft docs update --- docs/web3.contract.rst | 17 +++++++++++++++++ docs/web3.eth.rst | 1 + 2 files changed, 18 insertions(+) diff --git a/docs/web3.contract.rst b/docs/web3.contract.rst index 489a294655..2c9db0510f 100644 --- a/docs/web3.contract.rst +++ b/docs/web3.contract.rst @@ -146,12 +146,29 @@ Each Contract Factory exposes the following properties. The runtime part of the contract bytecode string. May be ``None`` if not provided during factory creation. + +.. py:attribute:: Contract.decode_tuples + + If a Tuple/Struct is returned by a contract function, this flag defines whether + to apply the field names from the ABI to the returned data. + If False, the returned value will be a normal Python `Tuple`. If True, the returned + value will be a Python `NamedTuple` of the name `ABIDecodedNamedTuple`. + + NamedTuples have some restrictions regarding field names. + Web3.py sets `NamedTuple`'s `rename=True`, so disallowed field names my be + different than expected. See the [Python docs](https://docs.python.org/3/library/collections.html#collections.namedtuple) + for more information. + + May be ``None`` if not provided during factory creation. + + .. py:attribute:: Contract.functions This provides access to contract functions as attributes. For example: ``myContract.functions.MyMethod()``. The exposed contract functions are classes of the type :py:class:`ContractFunction`. + .. py:attribute:: Contract.events This provides access to contract events as attributes. For example: diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst index 5844875044..e534cb1dfd 100644 --- a/docs/web3.eth.rst +++ b/docs/web3.eth.rst @@ -1454,6 +1454,7 @@ Contracts - ``bytecode_runtime`` - ``clone_bin`` - ``dev_doc`` + - ``decode_tuples`` - ``interface`` - ``metadata`` - ``opcodes`` From dce816f118ea7d115d7730575ddfae3079b9cf0f Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 2 Feb 2023 11:24:58 -0700 Subject: [PATCH 13/35] change decode_tuples default from None to False, fix docs typos --- docs/web3.contract.rst | 6 +++--- web3/contract/async_contract.py | 4 ++-- web3/contract/base_contract.py | 6 +++--- web3/contract/contract.py | 4 ++-- web3/contract/utils.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/web3.contract.rst b/docs/web3.contract.rst index 2c9db0510f..fc63e68a45 100644 --- a/docs/web3.contract.rst +++ b/docs/web3.contract.rst @@ -152,14 +152,14 @@ Each Contract Factory exposes the following properties. If a Tuple/Struct is returned by a contract function, this flag defines whether to apply the field names from the ABI to the returned data. If False, the returned value will be a normal Python `Tuple`. If True, the returned - value will be a Python `NamedTuple` of the name `ABIDecodedNamedTuple`. + value will be a Python `NamedTuple` of the class `ABIDecodedNamedTuple`. NamedTuples have some restrictions regarding field names. - Web3.py sets `NamedTuple`'s `rename=True`, so disallowed field names my be + Web3.py sets `NamedTuple`'s `rename=True`, so disallowed field names may be different than expected. See the [Python docs](https://docs.python.org/3/library/collections.html#collections.namedtuple) for more information. - May be ``None`` if not provided during factory creation. + Defaults to ``False`` if not provided during factory creation. .. py:attribute:: Contract.functions diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 49b7d7ca0b..c74bf59c06 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -79,7 +79,7 @@ def __init__( abi: ABI, w3: "Web3", address: Optional[ChecksumAddress] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> None: super().__init__(abi, w3, AsyncContractFunction, address, decode_tuples) @@ -474,7 +474,7 @@ def __init__( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> None: super().__init__( abi=abi, diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index ed2f379b5b..b38f00eb7f 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -436,7 +436,7 @@ def __init__( w3: "Web3", contract_function_class: Type["BaseContractFunction"], address: Optional[ChecksumAddress] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> None: self.abi = abi self.w3 = w3 @@ -671,7 +671,7 @@ class BaseContractFunction: abi: ABIFunction = None transaction: TxParams = None arguments: Tuple[Any, ...] = None - decode_tuples: Optional[bool] = None + decode_tuples: Optional[bool] = False args: Any = None kwargs: Any = None @@ -1090,7 +1090,7 @@ def __init__( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> None: self.w3 = w3 self.address = address diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 845b389280..4d80d2dbc8 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -79,7 +79,7 @@ def __init__( abi: ABI, w3: "Web3", address: Optional[ChecksumAddress] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> None: super().__init__(abi, w3, ContractFunction, address, decode_tuples) @@ -468,7 +468,7 @@ def __init__( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> None: super().__init__( abi=abi, diff --git a/web3/contract/utils.py b/web3/contract/utils.py index 8db67b457b..3921d49b36 100644 --- a/web3/contract/utils.py +++ b/web3/contract/utils.py @@ -72,7 +72,7 @@ def call_contract_function( fn_abi: Optional[ABIFunction] = None, state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, *args: Any, **kwargs: Any, ) -> Any: @@ -280,7 +280,7 @@ async def async_call_contract_function( fn_abi: Optional[ABIFunction] = None, state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = None, + decode_tuples: Optional[bool] = False, *args: Any, **kwargs: Any, ) -> Any: From dd8212c8a0ab83bb45bed91c9bdcae599b68ddb3 Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 6 Feb 2023 11:13:42 -0700 Subject: [PATCH 14/35] typing fix --- web3/_utils/abi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 2d563affb2..7585feed87 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -960,7 +960,7 @@ def to_tuple( return recursive_map(to_tuple, data) -def foldable_namedtuple(fields: Tuple[Any, ...]) -> Tuple[Any, ...]: +def foldable_namedtuple(fields: Tuple[Any, ...]) -> Callable[..., Tuple[Any, ...]]: """ Customized namedtuple such that `type(x)(x) == x`. """ @@ -972,7 +972,7 @@ def __new__(self, args: Any) -> "ABIDecodedNamedTuple": def _asdict(self) -> Dict[str, Any]: return dict(super()._asdict()) - return ABIDecodedNamedTuple # type: ignore + return ABIDecodedNamedTuple def generate_namedtuple_from_dict(**kwargs: Dict[str, Any]) -> Tuple[Any, ...]: @@ -983,4 +983,4 @@ def generate_namedtuple_from_dict(**kwargs: Dict[str, Any]) -> Tuple[Any, ...]: """ keys, values = zip(*kwargs.items()) - return foldable_namedtuple(keys)(values) # type: ignore + return foldable_namedtuple(keys)(values) From a323c0d4caed232212a301e9f32faac2ea2b31f9 Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 6 Feb 2023 13:08:24 -0700 Subject: [PATCH 15/35] refactor creation of abidecodednamedtuple --- tests/core/utilities/test_abi_named_tree.py | 15 +++++---- web3/_utils/abi.py | 36 +++++++-------------- web3/contract/utils.py | 6 ++-- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index e85952d671..7a633d62c7 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -8,10 +8,10 @@ ) from web3._utils.abi import ( + abi_decoded_namedtuple_factory, check_if_arguments_can_be_encoded, - dict_to_namedtuple, - foldable_namedtuple, named_tree, + recursive_dict_to_namedtuple, ) from web3.exceptions import ( MismatchedABI, @@ -32,7 +32,7 @@ def test_named_arguments_decode(): decoded = named_tree(full_abi_inputs, full_values) - data = dict_to_namedtuple(decoded) + data = abi_decoded_namedtuple_factory(decoded) assert data == full_values assert data.s.c[2].y == 10 assert data.t.x == 11 @@ -63,7 +63,7 @@ def test_named_arguments_decode(): def test_named_arguments_decode_rename(): decoded = named_tree(short_abi_inputs_with_disallowed_names, short_values) - data = dict_to_namedtuple(decoded) + data = recursive_dict_to_namedtuple(decoded) assert data == short_values assert data._fields == ("s",) @@ -93,12 +93,15 @@ def test_namedtuples_encodable(): registry = default_registry.copy() codec = ABICodec(registry) kwargs = named_tree(full_abi_inputs, full_values) - args = dict_to_namedtuple(kwargs) + args = recursive_dict_to_namedtuple(kwargs) assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, (), kwargs) assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, args, {}) def test_foldable_namedtuple(): - item = foldable_namedtuple(["a", "b", "c"])([1, 2, 3]) + item = abi_decoded_namedtuple_factory(["a", "b", "c"])([1, 2, 3]) assert type(item)(item) == item == (1, 2, 3) assert item.c == 3 + + expected_asdict = {"a": 1, "b": 2, "c": 3} + assert item._asdict() == expected_asdict diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 7585feed87..8a37297050 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -949,38 +949,24 @@ def named_subtree( return data -def dict_to_namedtuple(data: Dict[str, Any]) -> Tuple[Any, ...]: - def to_tuple( - item: Union[Dict[str, Any], List[Any]] +def recursive_dict_to_namedtuple(data: Dict[str, Any]) -> Tuple[Any, ...]: + def _dict_to_namedtuple( + value: Union[Dict[str, Any], List[Any]] ) -> Union[Tuple[Any, ...], List[Any]]: - if isinstance(item, dict): - return generate_namedtuple_from_dict(**item) - return item + if not isinstance(value, dict): + return value - return recursive_map(to_tuple, data) + keys, values = zip(*value.items()) + return abi_decoded_namedtuple_factory(keys)(values) + return recursive_map(_dict_to_namedtuple, data) -def foldable_namedtuple(fields: Tuple[Any, ...]) -> Callable[..., Tuple[Any, ...]]: - """ - Customized namedtuple such that `type(x)(x) == x`. - """ +def abi_decoded_namedtuple_factory( + fields: Tuple[Any, ...] +) -> Callable[..., Tuple[Any, ...]]: class ABIDecodedNamedTuple(namedtuple("ABIDecodedNamedTuple", fields, rename=True)): # type: ignore # noqa: E501 def __new__(self, args: Any) -> "ABIDecodedNamedTuple": return super().__new__(self, *args) - def _asdict(self) -> Dict[str, Any]: - return dict(super()._asdict()) - return ABIDecodedNamedTuple - - -def generate_namedtuple_from_dict(**kwargs: Dict[str, Any]) -> Tuple[Any, ...]: - """ - Literal namedtuple constructor such that: - `generate_namedtuple_from_dict(x=1, y=2)` - returns `generate_namedtuple_from_dict(x=1, y=2)`. - """ - keys, values = zip(*kwargs.items()) - - return foldable_namedtuple(keys)(values) diff --git a/web3/contract/utils.py b/web3/contract/utils.py index 3921d49b36..cbd5e825eb 100644 --- a/web3/contract/utils.py +++ b/web3/contract/utils.py @@ -21,11 +21,11 @@ ) from web3._utils.abi import ( - dict_to_namedtuple, filter_by_type, get_abi_output_types, map_abi_data, named_tree, + recursive_dict_to_namedtuple, ) from web3._utils.async_transactions import ( fill_transaction_defaults as async_fill_transaction_defaults, @@ -134,7 +134,7 @@ def call_contract_function( if decode_tuples: decoded = named_tree(fn_abi["outputs"], normalized_data) - normalized_data = dict_to_namedtuple(decoded) + normalized_data = recursive_dict_to_namedtuple(decoded) if len(normalized_data) == 1: return normalized_data[0] @@ -342,7 +342,7 @@ async def async_call_contract_function( if decode_tuples: decoded = named_tree(fn_abi["outputs"], normalized_data) - normalized_data = dict_to_namedtuple(decoded) + normalized_data = recursive_dict_to_namedtuple(decoded) if len(normalized_data) == 1: return normalized_data[0] From 90e06dae7e677dde4994031d78698649836bf38e Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 6 Feb 2023 13:24:15 -0700 Subject: [PATCH 16/35] fix test --- tests/core/utilities/test_abi_named_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/utilities/test_abi_named_tree.py b/tests/core/utilities/test_abi_named_tree.py index 7a633d62c7..85efa40f38 100644 --- a/tests/core/utilities/test_abi_named_tree.py +++ b/tests/core/utilities/test_abi_named_tree.py @@ -32,7 +32,7 @@ def test_named_arguments_decode(): decoded = named_tree(full_abi_inputs, full_values) - data = abi_decoded_namedtuple_factory(decoded) + data = recursive_dict_to_namedtuple(decoded) assert data == full_values assert data.s.c[2].y == 10 assert data.t.x == 11 @@ -98,7 +98,7 @@ def test_namedtuples_encodable(): assert check_if_arguments_can_be_encoded(TEST_FUNCTION_ABI, codec, args, {}) -def test_foldable_namedtuple(): +def test_ABIDecodedNamedTuple(): item = abi_decoded_namedtuple_factory(["a", "b", "c"])([1, 2, 3]) assert type(item)(item) == item == (1, 2, 3) assert item.c == 3 From 6dd9a32069b1bc021705b27ed065f140eada9c94 Mon Sep 17 00:00:00 2001 From: banteg Date: Mon, 13 May 2019 13:37:39 -0600 Subject: [PATCH 17/35] cherry-pick original commit --- web3/_utils/abi.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 8a37297050..8bd19a24f0 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -837,6 +837,7 @@ def strip_abi_type(elements: Any) -> Any: return elements +<<<<<<< HEAD def build_non_strict_registry() -> ABIRegistry: # We make a copy here just to make sure that eth-abi's default registry is not # affected by our custom encoder subclasses @@ -970,3 +971,25 @@ def __new__(self, args: Any) -> "ABIDecodedNamedTuple": return super().__new__(self, *args) return ABIDecodedNamedTuple +======= +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) +>>>>>>> e8f42aa7... Add rich tuple decoder From 1016e8ab103b112c69611635029a81b825945749 Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 15 May 2019 07:20:56 +0700 Subject: [PATCH 18/35] cherry-pick original commits --- web3/_utils/abi.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 8bd19a24f0..8a37297050 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -837,7 +837,6 @@ def strip_abi_type(elements: Any) -> Any: return elements -<<<<<<< HEAD def build_non_strict_registry() -> ABIRegistry: # We make a copy here just to make sure that eth-abi's default registry is not # affected by our custom encoder subclasses @@ -971,25 +970,3 @@ def __new__(self, args: Any) -> "ABIDecodedNamedTuple": return super().__new__(self, *args) return ABIDecodedNamedTuple -======= -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) ->>>>>>> e8f42aa7... Add rich tuple decoder From 2dc9208238abfe1039a8e92f97016bbcff59b932 Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 6 Feb 2023 16:10:54 -0700 Subject: [PATCH 19/35] improve contract_call_interface tests --- .../contracts/test_contract_call_interface.py | 266 ++++++------------ 1 file changed, 82 insertions(+), 184 deletions(-) diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index c36e7f18d5..6c39c27055 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -20,6 +20,9 @@ async_deploy, deploy, ) +from web3._utils.abi import ( + recursive_dict_to_namedtuple, +) from web3._utils.contract_sources.contract_data.bytes_contracts import ( BYTES32_CONTRACT_DATA, BYTES_CONTRACT_DATA, @@ -929,7 +932,7 @@ def test_call_tuple_contract(tuple_contract, method_input, expected): @pytest.mark.parametrize( - "method_input, expected", + "method_input, plain_tuple_output, type_str, namedtuple_repr", ( ( { @@ -978,63 +981,32 @@ def test_call_tuple_contract(tuple_contract, method_input, expected): ), ], ), - ), - ( - ( - 123, - [1, 2], - [ - ( - 234, - [True, False], - [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - ), - ( - 345, - [False, False], - [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - ), - ], - ), - ( - 123, - [1, 2], - [ - ( - 234, - [True, False], - [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - ), - ( - 345, - [False, False], - [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - ), - ], - ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 ), ), ) def test_call_tuple_contract_with_decode_tuples_set( - tuple_contract_with_decode_tuples, method_input, expected + tuple_contract_with_decode_tuples, + method_input, + plain_tuple_output, + type_str, + namedtuple_repr, ): result = tuple_contract_with_decode_tuples.functions.method(method_input).call() - assert result == expected + + # check contract output matches dict_to_namedtuple output + namedtuple_from_input = recursive_dict_to_namedtuple(method_input) + assert result == namedtuple_from_input + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + + # check that the namedtuple ouput is still a tuple + assert result == plain_tuple_output + + # check that fields are correct assert result._fields == ("a", "b", "c") + assert result.c[0]._fields == ("x", "y", "z") @pytest.mark.parametrize( @@ -1124,7 +1096,7 @@ def test_call_nested_tuple_contract(nested_tuple_contract, method_input, expecte @pytest.mark.parametrize( - "method_input, expected", + "method_input, plain_tuple_output, type_str, namedtuple_repr", ( ( { @@ -1163,55 +1135,33 @@ def test_call_nested_tuple_contract(nested_tuple_contract, method_input, expecte ), ], ), - ), - ( - ( - [ - ( - [ - (1, 2), - (3, 4), - (5, 6), - ], - ), - ( - [ - (7, 8), - (9, 10), - (11, 12), - ], - ), - ], - ), - ( - [ - ( - [ - (1, 2), - (3, 4), - (5, 6), - ], - ), - ( - [ - (7, 8), - (9, 10), - (11, 12), - ], - ), - ], - ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(t=[ABIDecodedNamedTuple(u=[ABIDecodedNamedTuple(x=1, y=2), ABIDecodedNamedTuple(x=3, y=4), ABIDecodedNamedTuple(x=5, y=6)]), ABIDecodedNamedTuple(u=[ABIDecodedNamedTuple(x=7, y=8), ABIDecodedNamedTuple(x=9, y=10), ABIDecodedNamedTuple(x=11, y=12)])])", # noqa: E501 ), ), ) def test_call_nested_tuple_contract_with_decode_tuples_set( - nested_tuple_contract_with_decode_tuples, method_input, expected + nested_tuple_contract_with_decode_tuples, + method_input, + plain_tuple_output, + type_str, + namedtuple_repr, ): result = nested_tuple_contract_with_decode_tuples.functions.method( method_input ).call() - assert result == expected + # check contract output matches dict_to_namedtuple output + namedtuple_from_input = recursive_dict_to_namedtuple(method_input) + assert result == namedtuple_from_input + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + + # check that the namedtuple ouput is still a tuple + assert result == plain_tuple_output + + # check that fields are correct assert result._fields == ("t",) + assert result.t[0]._fields == ("u",) def test_call_revert_contract(revert_contract): @@ -2137,7 +2087,7 @@ async def test_async_call_tuple_contract(async_tuple_contract, method_input, exp @pytest.mark.asyncio @pytest.mark.parametrize( - "method_input, expected", + "method_input, plain_tuple_output, type_str, namedtuple_repr", ( ( { @@ -2186,65 +2136,34 @@ async def test_async_call_tuple_contract(async_tuple_contract, method_input, exp ), ], ), - ), - ( - ( - 123, - [1, 2], - [ - ( - 234, - [True, False], - [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - ), - ( - 345, - [False, False], - [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - ), - ], - ), - ( - 123, - [1, 2], - [ - ( - 234, - [True, False], - [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - ), - ( - 345, - [False, False], - [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - ), - ], - ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 ), ), ) async def test_async_call_tuple_contract_with_decode_tuples_set( - async_tuple_contract_with_decode_tuples, method_input, expected + async_tuple_contract_with_decode_tuples, + method_input, + plain_tuple_output, + type_str, + namedtuple_repr, ): result = await async_tuple_contract_with_decode_tuples.functions.method( method_input ).call() - assert result == expected + + # check contract output matches dict_to_namedtuple output + namedtuple_from_input = recursive_dict_to_namedtuple(method_input) + assert result == namedtuple_from_input + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + + # check that the namedtuple ouput is still a tuple + assert result == plain_tuple_output + + # check that fields are correct assert result._fields == ("a", "b", "c") + assert result.c[0]._fields == ("x", "y", "z") @pytest.mark.asyncio @@ -2338,7 +2257,7 @@ async def test_async_call_nested_tuple_contract( @pytest.mark.asyncio @pytest.mark.parametrize( - "method_input, expected", + "method_input, plain_tuple_output, type_str, namedtuple_repr", ( ( { @@ -2377,55 +2296,34 @@ async def test_async_call_nested_tuple_contract( ), ], ), - ), - ( - ( - [ - ( - [ - (1, 2), - (3, 4), - (5, 6), - ], - ), - ( - [ - (7, 8), - (9, 10), - (11, 12), - ], - ), - ], - ), - ( - [ - ( - [ - (1, 2), - (3, 4), - (5, 6), - ], - ), - ( - [ - (7, 8), - (9, 10), - (11, 12), - ], - ), - ], - ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(t=[ABIDecodedNamedTuple(u=[ABIDecodedNamedTuple(x=1, y=2), ABIDecodedNamedTuple(x=3, y=4), ABIDecodedNamedTuple(x=5, y=6)]), ABIDecodedNamedTuple(u=[ABIDecodedNamedTuple(x=7, y=8), ABIDecodedNamedTuple(x=9, y=10), ABIDecodedNamedTuple(x=11, y=12)])])", # noqa: E501 ), ), ) async def test_async_call_nested_tuple_contract_with_decode_tuples_set( - async_nested_tuple_contract_with_decode_tuples, method_input, expected + async_nested_tuple_contract_with_decode_tuples, + method_input, + plain_tuple_output, + type_str, + namedtuple_repr, ): result = await async_nested_tuple_contract_with_decode_tuples.functions.method( method_input ).call() - assert result == expected + + # check contract output matches dict_to_namedtuple output + namedtuple_from_input = recursive_dict_to_namedtuple(method_input) + assert result == namedtuple_from_input + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + + # check that the namedtuple ouput is still a tuple + assert result == plain_tuple_output + + # check that fields are correct assert result._fields == ("t",) + assert result.t[0]._fields == ("u",) @pytest.mark.asyncio From 3565f7d8c9ba116e33df675fddedf115daefe818 Mon Sep 17 00:00:00 2001 From: pacrob Date: Tue, 7 Feb 2023 10:46:52 -0700 Subject: [PATCH 20/35] move tuple contract fixtures up to conftest --- tests/core/contracts/conftest.py | 82 +++++++++++++++++++ .../contracts/test_contract_call_interface.py | 82 ------------------- 2 files changed, 82 insertions(+), 82 deletions(-) diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index fd297e4c1d..834a7f2f5f 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -50,6 +50,10 @@ from web3._utils.contract_sources.contract_data.string_contract import ( STRING_CONTRACT_DATA, ) +from web3._utils.contract_sources.contract_data.tuple_contracts import ( + NESTED_TUPLE_CONTRACT_DATA, + TUPLE_CONTRACT_DATA, +) @pytest.fixture(scope="session") @@ -340,6 +344,44 @@ def revert_contract(w3, address_conversion_func): return deploy(w3, revert_contract_factory, address_conversion_func) +@pytest.fixture +def tuple_contract(w3, address_conversion_func): + tuple_contract_factory = w3.eth.contract(**TUPLE_CONTRACT_DATA) + return deploy(w3, tuple_contract_factory, address_conversion_func) + + +@pytest.fixture +def nested_tuple_contract(w3, address_conversion_func): + nested_tuple_contract_factory = w3.eth.contract(**NESTED_TUPLE_CONTRACT_DATA) + return deploy(w3, nested_tuple_contract_factory, address_conversion_func) + + +TUPLE_CONTRACT_DATA_DECODE_TUPLES = { + **TUPLE_CONTRACT_DATA, + "decode_tuples": True, +} + + +NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES = { + **NESTED_TUPLE_CONTRACT_DATA, + "decode_tuples": True, +} + + +@pytest.fixture +def tuple_contract_with_decode_tuples(w3, address_conversion_func): + tuple_contract_factory = w3.eth.contract(**TUPLE_CONTRACT_DATA_DECODE_TUPLES) + return deploy(w3, tuple_contract_factory, address_conversion_func) + + +@pytest.fixture +def nested_tuple_contract_with_decode_tuples(w3, address_conversion_func): + nested_tuple_contract_factory = w3.eth.contract( + **NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES + ) + return deploy(w3, nested_tuple_contract_factory, address_conversion_func) + + @pytest.fixture def some_address(address_conversion_func): return address_conversion_func("0x5B2063246F2191f18F2675ceDB8b28102e957458") @@ -563,6 +605,46 @@ async def async_revert_contract(async_w3, address_conversion_func): ) +@pytest_asyncio.fixture +async def async_tuple_contract(async_w3, address_conversion_func): + async_tuple_contract_factory = async_w3.eth.contract(**TUPLE_CONTRACT_DATA) + return await async_deploy( + async_w3, async_tuple_contract_factory, address_conversion_func + ) + + +@pytest_asyncio.fixture +async def async_nested_tuple_contract(async_w3, address_conversion_func): + async_nested_tuple_contract_factory = async_w3.eth.contract( + **NESTED_TUPLE_CONTRACT_DATA + ) + return await async_deploy( + async_w3, async_nested_tuple_contract_factory, address_conversion_func + ) + + +@pytest_asyncio.fixture +async def async_tuple_contract_with_decode_tuples(async_w3, address_conversion_func): + async_tuple_contract_factory = async_w3.eth.contract( + **TUPLE_CONTRACT_DATA_DECODE_TUPLES + ) + return await async_deploy( + async_w3, async_tuple_contract_factory, address_conversion_func + ) + + +@pytest_asyncio.fixture +async def async_nested_tuple_contract_with_decode_tuples( + async_w3, address_conversion_func +): + async_nested_tuple_contract_factory = async_w3.eth.contract( + **NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES + ) + return await async_deploy( + async_w3, async_nested_tuple_contract_factory, address_conversion_func + ) + + async def async_invoke_contract( api_call_desig="call", contract=None, diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index 6c39c27055..e2fda65182 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -27,10 +27,6 @@ BYTES32_CONTRACT_DATA, BYTES_CONTRACT_DATA, ) -from web3._utils.contract_sources.contract_data.tuple_contracts import ( - NESTED_TUPLE_CONTRACT_DATA, - TUPLE_CONTRACT_DATA, -) from web3._utils.ens import ( contract_ens_addresses, ) @@ -51,44 +47,6 @@ ) -@pytest.fixture -def tuple_contract(w3, address_conversion_func): - tuple_contract_factory = w3.eth.contract(**TUPLE_CONTRACT_DATA) - return deploy(w3, tuple_contract_factory, address_conversion_func) - - -@pytest.fixture -def nested_tuple_contract(w3, address_conversion_func): - nested_tuple_contract_factory = w3.eth.contract(**NESTED_TUPLE_CONTRACT_DATA) - return deploy(w3, nested_tuple_contract_factory, address_conversion_func) - - -TUPLE_CONTRACT_DATA_DECODE_TUPLES = { - **TUPLE_CONTRACT_DATA, - "decode_tuples": True, -} - - -NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES = { - **NESTED_TUPLE_CONTRACT_DATA, - "decode_tuples": True, -} - - -@pytest.fixture -def tuple_contract_with_decode_tuples(w3, address_conversion_func): - tuple_contract_factory = w3.eth.contract(**TUPLE_CONTRACT_DATA_DECODE_TUPLES) - return deploy(w3, tuple_contract_factory, address_conversion_func) - - -@pytest.fixture -def nested_tuple_contract_with_decode_tuples(w3, address_conversion_func): - nested_tuple_contract_factory = w3.eth.contract( - **NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES - ) - return deploy(w3, nested_tuple_contract_factory, address_conversion_func) - - @pytest.fixture(params=[b"\x04\x06", "0x0406"]) def bytes_contract(w3, request, address_conversion_func): bytes_contract_factory = w3.eth.contract(**BYTES_CONTRACT_DATA) @@ -1189,46 +1147,6 @@ def test_changing_default_block_identifier(w3, math_contract): # -- async -- # -@pytest_asyncio.fixture -async def async_tuple_contract(async_w3, address_conversion_func): - async_tuple_contract_factory = async_w3.eth.contract(**TUPLE_CONTRACT_DATA) - return await async_deploy( - async_w3, async_tuple_contract_factory, address_conversion_func - ) - - -@pytest_asyncio.fixture -async def async_nested_tuple_contract(async_w3, address_conversion_func): - async_nested_tuple_contract_factory = async_w3.eth.contract( - **NESTED_TUPLE_CONTRACT_DATA - ) - return await async_deploy( - async_w3, async_nested_tuple_contract_factory, address_conversion_func - ) - - -@pytest_asyncio.fixture -async def async_tuple_contract_with_decode_tuples(async_w3, address_conversion_func): - async_tuple_contract_factory = async_w3.eth.contract( - **TUPLE_CONTRACT_DATA_DECODE_TUPLES - ) - return await async_deploy( - async_w3, async_tuple_contract_factory, address_conversion_func - ) - - -@pytest_asyncio.fixture -async def async_nested_tuple_contract_with_decode_tuples( - async_w3, address_conversion_func -): - async_nested_tuple_contract_factory = async_w3.eth.contract( - **NESTED_TUPLE_CONTRACT_DATA_DECODE_TUPLES - ) - return await async_deploy( - async_w3, async_nested_tuple_contract_factory, address_conversion_func - ) - - @pytest.fixture def async_bytes_contract_factory(async_w3): return async_w3.eth.contract(**BYTES_CONTRACT_DATA) From 48987c4d0ae8b3629c643e9a771e2c92fa8edae3 Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 8 Feb 2023 09:09:43 -0700 Subject: [PATCH 21/35] wip --- .../test_contract_caller_interface.py | 74 +++++++++++++++++++ web3/contract/base_contract.py | 2 +- web3/contract/contract.py | 14 +++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/tests/core/contracts/test_contract_caller_interface.py b/tests/core/contracts/test_contract_caller_interface.py index a644f3b081..dc714e3503 100644 --- a/tests/core/contracts/test_contract_caller_interface.py +++ b/tests/core/contracts/test_contract_caller_interface.py @@ -149,6 +149,80 @@ def test_caller_with_args_and_no_transaction_keyword( add_result = contract.add(3, 5) assert add_result == 8 + + +@pytest.mark.parametrize( + "method_input, expected, type_str, namedtuple_repr", + ( + ( + { + "a": 123, + "b": [1, 2], + "c": [ + { + "x": 234, + "y": [True, False], + "z": [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + }, + { + "x": 345, + "y": [False, False], + "z": [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + }, + ], + }, + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 + ), + ), +) +def test_tuple_contract_caller_with_decode_tuples( + tuple_contract_with_decode_tuples, method_input, expected, type_str, namedtuple_repr, transaction_dict +): + # result = tuple_contract_with_decode_tuples.caller(transaction=transaction_dict).method(method_input) + result = tuple_contract_with_decode_tuples.caller(decode_tuples=True).method(method_input) + caller = tuple_contract_with_decode_tuples.caller + breakpoint() + assert result == expected + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + result = tuple_contract_with_decode_tuples.caller().method(method_input) + assert result == expected + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + + + # --- async --- # @pytest.mark.asyncio diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index b38f00eb7f..c1561b0868 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -1092,6 +1092,7 @@ def __init__( ccip_read_enabled: Optional[bool] = None, decode_tuples: Optional[bool] = False, ) -> None: + print(decode_tuples) self.w3 = w3 self.address = address self.abi = abi @@ -1109,7 +1110,6 @@ def __init__( w3=self.w3, contract_abi=self.abi, address=self.address, - decode_tuples=self.decode_tuples, function_identifier=func["name"], ) diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 4d80d2dbc8..960f19a6bf 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -235,7 +235,7 @@ def __init__(self, address: Optional[ChecksumAddress] = None) -> None: self.functions = ContractFunctions( self.abi, _w3, self.address, self.decode_tuples ) - self.caller = ContractCaller(self.abi, _w3, self.address) + self.caller = ContractCaller(self.abi, _w3, self.address, self.decode_tuples) self.events = ContractEvents(self.abi, _w3, self.address) self.fallback = Contract.get_fallback_function( self.abi, @@ -272,10 +272,17 @@ def factory( normalizers=normalizers, ), ) + # if contract.decode_tuples: + # breakpoint() + # else: + # print("passsing!", contract.all_functions()) + contract.functions = ContractFunctions( contract.abi, contract.w3, contract.decode_tuples ) - contract.caller = ContractCaller(contract.abi, contract.w3, contract.address) + contract.caller = ContractCaller(contract.abi, contract.w3, contract.address, contract.decode_tuples) + # if contract.decode_tuples: + # breakpoint() contract.events = ContractEvents(contract.abi, contract.w3) contract.fallback = Contract.get_fallback_function( contract.abi, @@ -470,6 +477,7 @@ def __init__( ccip_read_enabled: Optional[bool] = None, decode_tuples: Optional[bool] = False, ) -> None: + # breakpoint() super().__init__( abi=abi, w3=w3, @@ -487,6 +495,7 @@ def __call__( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> "ContractCaller": if transaction is None: transaction = {} @@ -497,4 +506,5 @@ def __call__( transaction=transaction, block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, + decode_tuples=decode_tuples, ) From c6800e6a00e3241c90d8102342772172f3e567bb Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 8 Feb 2023 14:21:47 -0700 Subject: [PATCH 22/35] contract caller tests added and passing --- .../test_contract_caller_interface.py | 87 +++++++++++++++++-- web3/contract/async_contract.py | 17 ++-- web3/contract/base_contract.py | 6 +- web3/contract/contract.py | 31 ++++--- 4 files changed, 112 insertions(+), 29 deletions(-) diff --git a/tests/core/contracts/test_contract_caller_interface.py b/tests/core/contracts/test_contract_caller_interface.py index dc714e3503..78733a413f 100644 --- a/tests/core/contracts/test_contract_caller_interface.py +++ b/tests/core/contracts/test_contract_caller_interface.py @@ -149,7 +149,7 @@ def test_caller_with_args_and_no_transaction_keyword( add_result = contract.add(3, 5) assert add_result == 8 - + @pytest.mark.parametrize( "method_input, expected, type_str, namedtuple_repr", @@ -207,12 +207,13 @@ def test_caller_with_args_and_no_transaction_keyword( ), ) def test_tuple_contract_caller_with_decode_tuples( - tuple_contract_with_decode_tuples, method_input, expected, type_str, namedtuple_repr, transaction_dict + tuple_contract_with_decode_tuples, + method_input, + expected, + type_str, + namedtuple_repr, ): - # result = tuple_contract_with_decode_tuples.caller(transaction=transaction_dict).method(method_input) - result = tuple_contract_with_decode_tuples.caller(decode_tuples=True).method(method_input) - caller = tuple_contract_with_decode_tuples.caller - breakpoint() + result = tuple_contract_with_decode_tuples.caller.method(method_input) assert result == expected assert str(type(result)) == type_str assert result.__repr__() == namedtuple_repr @@ -221,7 +222,6 @@ def test_tuple_contract_caller_with_decode_tuples( assert str(type(result)) == type_str assert result.__repr__() == namedtuple_repr - # --- async --- # @@ -369,3 +369,76 @@ async def test_async_caller_with_args_and_no_transaction_keyword( add_result = await contract.add(3, 5) assert add_result == 8 + + +@pytest.mark.parametrize( + "method_input, expected, type_str, namedtuple_repr", + ( + ( + { + "a": 123, + "b": [1, 2], + "c": [ + { + "x": 234, + "y": [True, False], + "z": [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + }, + { + "x": 345, + "y": [False, False], + "z": [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + }, + ], + }, + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 + ), + ), +) +@pytest.mark.asyncio +async def test_async_tuple_contract_caller_with_decode_tuples( + async_tuple_contract_with_decode_tuples, + method_input, + expected, + type_str, + namedtuple_repr, +): + result = await async_tuple_contract_with_decode_tuples.caller.method(method_input) + assert result == expected + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr + result = await async_tuple_contract_with_decode_tuples.caller().method(method_input) + assert result == expected + assert str(type(result)) == type_str + assert result.__repr__() == namedtuple_repr diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index c74bf59c06..ef131ca66f 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -117,9 +117,11 @@ def __init__(self, address: Optional[ChecksumAddress] = None) -> None: "The address argument is required to instantiate a contract." ) self.functions = AsyncContractFunctions( - self.abi, self.w3, self.address, self.decode_tuples + self.abi, self.w3, self.address, decode_tuples=self.decode_tuples + ) + self.caller = AsyncContractCaller( + self.abi, self.w3, self.address, decode_tuples=self.decode_tuples ) - self.caller = AsyncContractCaller(self.abi, self.w3, self.address) self.events = AsyncContractEvents(self.abi, self.w3, self.address) self.fallback = AsyncContract.get_fallback_function( self.abi, self.w3, AsyncContractFunction, self.address @@ -151,10 +153,13 @@ def factory( ), ) contract.functions = AsyncContractFunctions( - contract.abi, contract.w3, contract.decode_tuples + contract.abi, contract.w3, decode_tuples=contract.decode_tuples ) contract.caller = AsyncContractCaller( - contract.abi, contract.w3, contract.address + contract.abi, + contract.w3, + contract.address, + decode_tuples=contract.decode_tuples, ) contract.events = AsyncContractEvents(contract.abi, contract.w3) contract.fallback = AsyncContract.get_fallback_function( @@ -254,6 +259,7 @@ async def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = None, ) -> Any: """ Execute a contract function call using the `eth_call` interface. @@ -294,7 +300,7 @@ async def call( self.abi, state_override, ccip_read_enabled, - self.decode_tuples, + decode_tuples, *self.args, **self.kwargs, ) @@ -502,4 +508,5 @@ def __call__( transaction=transaction, block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, + decode_tuples=self.decode_tuples, ) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index c1561b0868..fe99e1c6e6 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -691,6 +691,7 @@ def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> Any: # This was needed for typing raise NotImplementedError( @@ -1092,7 +1093,6 @@ def __init__( ccip_read_enabled: Optional[bool] = None, decode_tuples: Optional[bool] = False, ) -> None: - print(decode_tuples) self.w3 = w3 self.address = address self.abi = abi @@ -1111,6 +1111,7 @@ def __init__( contract_abi=self.abi, address=self.address, function_identifier=func["name"], + decode_tuples=decode_tuples, ) block_id = parse_block_identifier(self.w3, block_identifier) @@ -1120,6 +1121,7 @@ def __init__( transaction=transaction, block_identifier=block_id, ccip_read_enabled=ccip_read_enabled, + decode_tuples=decode_tuples, ) setattr(self, func["name"], caller_method) @@ -1158,6 +1160,7 @@ def call_function( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = False, **kwargs: Any, ) -> Any: if transaction is None: @@ -1166,4 +1169,5 @@ def call_function( transaction=transaction, block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, + decode_tuples=decode_tuples, ) diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 960f19a6bf..9ff5509d97 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -233,9 +233,11 @@ def __init__(self, address: Optional[ChecksumAddress] = None) -> None: ) self.functions = ContractFunctions( - self.abi, _w3, self.address, self.decode_tuples + self.abi, _w3, self.address, decode_tuples=self.decode_tuples + ) + self.caller = ContractCaller( + self.abi, _w3, self.address, decode_tuples=self.decode_tuples ) - self.caller = ContractCaller(self.abi, _w3, self.address, self.decode_tuples) self.events = ContractEvents(self.abi, _w3, self.address) self.fallback = Contract.get_fallback_function( self.abi, @@ -272,17 +274,15 @@ def factory( normalizers=normalizers, ), ) - # if contract.decode_tuples: - # breakpoint() - # else: - # print("passsing!", contract.all_functions()) - contract.functions = ContractFunctions( - contract.abi, contract.w3, contract.decode_tuples + contract.abi, contract.w3, decode_tuples=contract.decode_tuples + ) + contract.caller = ContractCaller( + contract.abi, + contract.w3, + contract.address, + decode_tuples=contract.decode_tuples, ) - contract.caller = ContractCaller(contract.abi, contract.w3, contract.address, contract.decode_tuples) - # if contract.decode_tuples: - # breakpoint() contract.events = ContractEvents(contract.abi, contract.w3) contract.fallback = Contract.get_fallback_function( contract.abi, @@ -376,6 +376,7 @@ def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> Any: """ Execute a contract function call using the `eth_call` interface. @@ -416,7 +417,7 @@ def call( self.abi, state_override, ccip_read_enabled, - self.decode_tuples, + decode_tuples, *self.args, **self.kwargs, ) @@ -477,7 +478,6 @@ def __init__( ccip_read_enabled: Optional[bool] = None, decode_tuples: Optional[bool] = False, ) -> None: - # breakpoint() super().__init__( abi=abi, w3=w3, @@ -493,12 +493,11 @@ def __call__( self, transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", - state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = False, ) -> "ContractCaller": if transaction is None: transaction = {} + return type(self)( self.abi, self.w3, @@ -506,5 +505,5 @@ def __call__( transaction=transaction, block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, - decode_tuples=decode_tuples, + decode_tuples=self.decode_tuples, ) From 3ce29f8a6e7f5f195198d114b02c757f3471bf6b Mon Sep 17 00:00:00 2001 From: pacrob Date: Wed, 8 Feb 2023 15:17:09 -0700 Subject: [PATCH 23/35] correct ContractFunction call args --- web3/contract/async_contract.py | 3 +-- web3/contract/contract.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index ef131ca66f..97aa815c10 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -259,7 +259,6 @@ async def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = None, ) -> Any: """ Execute a contract function call using the `eth_call` interface. @@ -300,7 +299,7 @@ async def call( self.abi, state_override, ccip_read_enabled, - decode_tuples, + self.decode_tuples, *self.args, **self.kwargs, ) diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 9ff5509d97..4b95dbcc6f 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -376,7 +376,6 @@ def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = False, ) -> Any: """ Execute a contract function call using the `eth_call` interface. @@ -417,7 +416,7 @@ def call( self.abi, state_override, ccip_read_enabled, - decode_tuples, + self.decode_tuples, *self.args, **self.kwargs, ) From ee8fd8e8006474ec28c9f1abaf32adfe5a0ff77b Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 9 Feb 2023 10:51:05 -0700 Subject: [PATCH 24/35] call and caller tests passing --- web3/contract/async_contract.py | 1 + web3/contract/base_contract.py | 2 +- web3/contract/contract.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 97aa815c10..1aed7579d8 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -259,6 +259,7 @@ async def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> Any: """ Execute a contract function call using the `eth_call` interface. diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index fe99e1c6e6..2ea0432fed 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -1160,7 +1160,7 @@ def call_function( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = False, + decode_tuples: bool, **kwargs: Any, ) -> Any: if transaction is None: diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 4b95dbcc6f..337b218042 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -376,6 +376,7 @@ def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, + decode_tuples: Optional[bool] = False, ) -> Any: """ Execute a contract function call using the `eth_call` interface. From 6548f7eff2f47af216a391bc0215131f0978d36a Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 9 Feb 2023 15:29:48 -0700 Subject: [PATCH 25/35] remove some unused params --- web3/contract/async_contract.py | 1 - web3/contract/base_contract.py | 4 ---- web3/contract/contract.py | 1 - 3 files changed, 6 deletions(-) diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py index 1aed7579d8..97aa815c10 100644 --- a/web3/contract/async_contract.py +++ b/web3/contract/async_contract.py @@ -259,7 +259,6 @@ async def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = False, ) -> Any: """ Execute a contract function call using the `eth_call` interface. diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 2ea0432fed..5773a4cfd0 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -691,7 +691,6 @@ def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = False, ) -> Any: # This was needed for typing raise NotImplementedError( @@ -1121,7 +1120,6 @@ def __init__( transaction=transaction, block_identifier=block_id, ccip_read_enabled=ccip_read_enabled, - decode_tuples=decode_tuples, ) setattr(self, func["name"], caller_method) @@ -1160,7 +1158,6 @@ def call_function( transaction: Optional[TxParams] = None, block_identifier: BlockIdentifier = "latest", ccip_read_enabled: Optional[bool] = None, - decode_tuples: bool, **kwargs: Any, ) -> Any: if transaction is None: @@ -1169,5 +1166,4 @@ def call_function( transaction=transaction, block_identifier=block_identifier, ccip_read_enabled=ccip_read_enabled, - decode_tuples=decode_tuples, ) diff --git a/web3/contract/contract.py b/web3/contract/contract.py index 337b218042..4b95dbcc6f 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -376,7 +376,6 @@ def call( block_identifier: BlockIdentifier = "latest", state_override: Optional[CallOverride] = None, ccip_read_enabled: Optional[bool] = None, - decode_tuples: Optional[bool] = False, ) -> Any: """ Execute a contract function call using the `eth_call` interface. From 77748f50cdfafd284a88292da5e3307d8354a31a Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 9 Feb 2023 16:14:57 -0700 Subject: [PATCH 26/35] dedup test params --- .../test_contract_caller_interface.py | 209 ++++++++---------- 1 file changed, 90 insertions(+), 119 deletions(-) diff --git a/tests/core/contracts/test_contract_caller_interface.py b/tests/core/contracts/test_contract_caller_interface.py index 78733a413f..046aa384ab 100644 --- a/tests/core/contracts/test_contract_caller_interface.py +++ b/tests/core/contracts/test_contract_caller_interface.py @@ -23,6 +23,63 @@ def transaction_dict(w3, address): } +decode_tuples_args = ( + "method_input, tuple_output, type_str, namedtuple_repr", + ( + ( + { + "a": 123, + "b": [1, 2], + "c": [ + { + "x": 234, + "y": [True, False], + "z": [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + }, + { + "x": 345, + "y": [False, False], + "z": [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + }, + ], + }, + ( + 123, + [1, 2], + [ + ( + 234, + [True, False], + [ + "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", + ], + ), + ( + 345, + [False, False], + [ + "0xefd1FF70c185A1C0b125939815225199079096Ee", + "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", + ], + ), + ], + ), + ".ABIDecodedNamedTuple'>", # noqa: E501 + "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 + ), + ), +) + + def test_caller_default(math_contract): result = math_contract.caller.add(3, 5) assert result == 8 @@ -151,78 +208,35 @@ def test_caller_with_args_and_no_transaction_keyword( assert add_result == 8 -@pytest.mark.parametrize( - "method_input, expected, type_str, namedtuple_repr", - ( - ( - { - "a": 123, - "b": [1, 2], - "c": [ - { - "x": 234, - "y": [True, False], - "z": [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - }, - { - "x": 345, - "y": [False, False], - "z": [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - }, - ], - }, - ( - 123, - [1, 2], - [ - ( - 234, - [True, False], - [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - ), - ( - 345, - [False, False], - [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - ), - ], - ), - ".ABIDecodedNamedTuple'>", # noqa: E501 - "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 - ), - ), -) -def test_tuple_contract_caller_with_decode_tuples( +@pytest.mark.parametrize(*decode_tuples_args) +def test_tuple_contract_caller_default_with_decode_tuples( tuple_contract_with_decode_tuples, method_input, - expected, + tuple_output, type_str, namedtuple_repr, ): result = tuple_contract_with_decode_tuples.caller.method(method_input) - assert result == expected + assert result == tuple_output assert str(type(result)) == type_str assert result.__repr__() == namedtuple_repr + + +@pytest.mark.parametrize(*decode_tuples_args) +def test_tuple_contract_caller_with_parens_with_decode_tuples( + tuple_contract_with_decode_tuples, + method_input, + tuple_output, + type_str, + namedtuple_repr, +): result = tuple_contract_with_decode_tuples.caller().method(method_input) - assert result == expected + assert result == tuple_output assert str(type(result)) == type_str assert result.__repr__() == namedtuple_repr - # --- async --- # + +# --- async --- # @pytest.mark.asyncio @@ -371,74 +385,31 @@ async def test_async_caller_with_args_and_no_transaction_keyword( assert add_result == 8 -@pytest.mark.parametrize( - "method_input, expected, type_str, namedtuple_repr", - ( - ( - { - "a": 123, - "b": [1, 2], - "c": [ - { - "x": 234, - "y": [True, False], - "z": [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - }, - { - "x": 345, - "y": [False, False], - "z": [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - }, - ], - }, - ( - 123, - [1, 2], - [ - ( - 234, - [True, False], - [ - "0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - "0xfdF1946A9b40245224488F1a36f4A9ed4844a523", - ], - ), - ( - 345, - [False, False], - [ - "0xefd1FF70c185A1C0b125939815225199079096Ee", - "0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e", - ], - ), - ], - ), - ".ABIDecodedNamedTuple'>", # noqa: E501 - "ABIDecodedNamedTuple(a=123, b=[1, 2], c=[ABIDecodedNamedTuple(x=234, y=[True, False], z=['0x4AD7E79d88650B01EEA2B1f069f01EE9db343d5c', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523', '0xfdF1946A9b40245224488F1a36f4A9ed4844a523']), ABIDecodedNamedTuple(x=345, y=[False, False], z=['0xefd1FF70c185A1C0b125939815225199079096Ee', '0xf35C0784794F3Cd935F5754d3a0EbcE95bEf851e'])])", # noqa: E501 - ), - ), -) +@pytest.mark.parametrize(*decode_tuples_args) @pytest.mark.asyncio -async def test_async_tuple_contract_caller_with_decode_tuples( +async def test_async_tuple_contract_caller_default_with_decode_tuples( async_tuple_contract_with_decode_tuples, method_input, - expected, + tuple_output, type_str, namedtuple_repr, ): result = await async_tuple_contract_with_decode_tuples.caller.method(method_input) - assert result == expected + assert result == tuple_output assert str(type(result)) == type_str assert result.__repr__() == namedtuple_repr + + +@pytest.mark.parametrize(*decode_tuples_args) +@pytest.mark.asyncio +async def test_async_tuple_contract_caller_with_parens_with_decode_tuples( + async_tuple_contract_with_decode_tuples, + method_input, + tuple_output, + type_str, + namedtuple_repr, +): result = await async_tuple_contract_with_decode_tuples.caller().method(method_input) - assert result == expected + assert result == tuple_output assert str(type(result)) == type_str assert result.__repr__() == namedtuple_repr From 623a230385a8033631e39a105b4a7811d72243aa Mon Sep 17 00:00:00 2001 From: pacrob Date: Thu, 9 Feb 2023 16:16:53 -0700 Subject: [PATCH 27/35] add newsfragment --- newsfragments/2799.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/2799.feature.rst diff --git a/newsfragments/2799.feature.rst b/newsfragments/2799.feature.rst new file mode 100644 index 0000000000..c6310b2588 --- /dev/null +++ b/newsfragments/2799.feature.rst @@ -0,0 +1 @@ +add decode_tuples option to contract instantiation From 0f24eb8095f6cd7b062df2b16b72f9e44fc9b26c Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 10 Feb 2023 10:26:36 -0700 Subject: [PATCH 28/35] wip --- .../contracts/test_extracting_event_data.py | 4 ++-- web3/_utils/contracts.py | 12 +++++++++++ web3/_utils/events.py | 7 ++++++- web3/contract/base_contract.py | 21 ++++++------------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/core/contracts/test_extracting_event_data.py b/tests/core/contracts/test_extracting_event_data.py index d68aa65311..46239be82c 100644 --- a/tests/core/contracts/test_extracting_event_data.py +++ b/tests/core/contracts/test_extracting_event_data.py @@ -976,7 +976,7 @@ def test_get_all_entries_with_nested_tuple_event(w3, emitter): log_entry = entries[0] - assert log_entry.args == {"arg0": 1, "arg1": (2, 3, (4,))} + assert log_entry.args == {"arg0": 1, "arg1": {"a": 2, "b": 3, "nested": {"c": 4}}} assert log_entry.event == "LogStructArgs" assert log_entry.blockHash == txn_receipt["blockHash"] assert log_entry.blockNumber == txn_receipt["blockNumber"] @@ -1002,7 +1002,7 @@ def test_get_all_entries_with_nested_tuple_event_non_strict( log_entry = entries[0] - assert log_entry.args == {"arg0": 1, "arg1": (2, 3, (4,))} + assert log_entry.args == {"arg0": 1, "arg1": {"a": 2, "b": 3, "nested": {"c": 4}}} assert log_entry.event == "LogStructArgs" assert log_entry.blockHash == txn_receipt["blockHash"] assert log_entry.blockNumber == txn_receipt["blockNumber"] diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 61ee9315fb..2492a73b5d 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -2,6 +2,7 @@ from typing import ( TYPE_CHECKING, Any, + Dict, Optional, Sequence, Tuple, @@ -47,6 +48,7 @@ get_receive_func_abi, map_abi_data, merge_args_and_kwargs, + named_tree, ) from web3._utils.blocks import ( is_hex_encoded_block_hash, @@ -312,6 +314,16 @@ def encode_transaction_data( return add_0x_prefix(encode_abi(w3, fn_abi, fn_arguments, fn_selector)) +def decode_transaction_data(fn_abi: ABIFunction, data, normalizers=None) -> Dict[str, Any]: + breakpoint() + data = HexBytes(data) + types = get_abi_input_types(fn_abi) + decoded = ABICodec.decode(types, data[4:]) + if normalizers: + decoded = map_abi_data(normalizers, types, decoded) + return named_tree(fn_abi["inputs"], decoded) + + def get_fallback_function_info( contract_abi: Optional[ABI] = None, fn_abi: Optional[ABIFunction] = None ) -> Tuple[ABIFunction, HexStr, Tuple[Any, ...]]: diff --git a/web3/_utils/events.py b/web3/_utils/events.py index c8632dd19a..f63360cd5c 100644 --- a/web3/_utils/events.py +++ b/web3/_utils/events.py @@ -58,6 +58,7 @@ get_indexed_event_inputs, get_normalized_abi_arg_type, map_abi_data, + named_tree, normalize_event_input_types, ) from web3._utils.encoding import ( @@ -255,6 +256,10 @@ def get_event_data( normalized_log_data = map_abi_data( BASE_RETURN_NORMALIZERS, log_data_types, decoded_log_data ) + named_log_data = named_tree( + log_data_normalized_inputs, + normalized_log_data, + ) decoded_topic_data = [ abi_codec.decode([topic_type], topic_data)[0] @@ -267,7 +272,7 @@ def get_event_data( event_args = dict( itertools.chain( zip(log_topic_names, normalized_topic_data), - zip(log_data_names, normalized_log_data), + named_log_data.items(), ) ) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 5773a4cfd0..a0ed55332b 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -43,15 +43,13 @@ check_if_arguments_can_be_encoded, fallback_func_abi_exists, filter_by_type, - get_abi_input_names, - get_abi_input_types, get_constructor_abi, is_array_type, - map_abi_data, merge_args_and_kwargs, receive_func_abi_exists, ) from web3._utils.contracts import ( + decode_transaction_data, encode_abi, find_matching_event_abi, find_matching_fn_abi, @@ -253,18 +251,11 @@ def callable_check(fn_abi: ABIFunction) -> bool: def decode_function_input( self, data: HexStr ) -> Tuple["BaseContractFunction", Dict[str, Any]]: - # type ignored b/c expects data arg to be HexBytes - data = HexBytes(data) # type: ignore - selector, params = data[:4], data[4:] - func = self.get_function_by_selector(selector) - - names = get_abi_input_names(func.abi) - types = get_abi_input_types(func.abi) - - decoded = self.w3.codec.decode(types, cast(HexBytes, params)) - normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) - - return func, dict(zip(names, normalized)) + func = self.get_function_by_selector(data[:4]) + arguments = decode_transaction_data( + func.abi, data, normalizers=BASE_RETURN_NORMALIZERS + ) + return func, arguments @combomethod def find_functions_by_args(self, *args: Any) -> "BaseContractFunction": From 628694161fcefc0ec0a7ee8e622024f1f7e587b0 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 10 Feb 2023 11:45:13 -0700 Subject: [PATCH 29/35] abi decoding tests pass --- .../test_contract_method_abi_decoding.py | 17 +++++++++++------ web3/_utils/contracts.py | 7 +++++-- web3/contract/base_contract.py | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/core/contracts/test_contract_method_abi_decoding.py b/tests/core/contracts/test_contract_method_abi_decoding.py index bc54d4bca6..66af21d60e 100644 --- a/tests/core/contracts/test_contract_method_abi_decoding.py +++ b/tests/core/contracts/test_contract_method_abi_decoding.py @@ -87,12 +87,15 @@ "0xc29a4b71000000000000000000000000bfae42a79ff045659dd0f84e65534f5c4c8100230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000db3d3af153cb02f0bc44621db82289280e93500f94a7d1598c397f6b49ecd5ccbc2b464259b96870063493b0dc7409d0fd9fb9860000000000000000000000000000000000000000000000000429d069189e00000000000000000000000000000000000178287f49c4a1d6622fb2ab40000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", # noqa: E501 "liquidate", { - "fromAccount": ("0xBfae42A79FF045659DD0F84e65534f5c4c810023", 0), - "liquidAccount": ( - "0xdb3d3AF153cB02f0Bc44621Db82289280e93500F", - 67238809929330522294664880975001390268660278453875034113630810005818923006342, # noqa: E501 - ), # noqa: E501 - "minLiquidatorRatio": (300000000000000000,), + "fromAccount": { + "owner": "0xBfae42A79FF045659DD0F84e65534f5c4c810023", + "number": 0, + }, + "liquidAccount": { + "owner": "0xdb3d3AF153cB02f0Bc44621Db82289280e93500F", + "number": 67238809929330522294664880975001390268660278453875034113630810005818923006342, # noqa: E501 + }, + "minLiquidatorRatio": {"value": 300000000000000000}, "minValueLiquidated": 500000000000000000000000000000000000000, "owedPreferences": [0, 1, 2], "heldPreferences": [2, 0, 1], @@ -104,6 +107,8 @@ def test_contract_abi_decoding(w3, abi, data, method, expected): contract = w3.eth.contract(abi=abi) func, params = contract.decode_function_input(data) assert func.fn_name == method + # if method == "liquidate": + # breakpoint() assert params == expected reinvoke_func = contract.functions[func.fn_name](**params) diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 2492a73b5d..a516cae624 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -313,12 +313,15 @@ def encode_transaction_data( return add_0x_prefix(encode_abi(w3, fn_abi, fn_arguments, fn_selector)) +from eth_abi.registry import ( + registry as default_registry, +) def decode_transaction_data(fn_abi: ABIFunction, data, normalizers=None) -> Dict[str, Any]: - breakpoint() data = HexBytes(data) types = get_abi_input_types(fn_abi) - decoded = ABICodec.decode(types, data[4:]) + abi_codec = ABICodec(default_registry) + decoded = abi_codec.decode(types=types, data=data[4:]) if normalizers: decoded = map_abi_data(normalizers, types, decoded) return named_tree(fn_abi["inputs"], decoded) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index a0ed55332b..0315f978b0 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -251,6 +251,7 @@ def callable_check(fn_abi: ABIFunction) -> bool: def decode_function_input( self, data: HexStr ) -> Tuple["BaseContractFunction", Dict[str, Any]]: + data = HexBytes(data) func = self.get_function_by_selector(data[:4]) arguments = decode_transaction_data( func.abi, data, normalizers=BASE_RETURN_NORMALIZERS From 757f4c30e5ae0a6063505563bd34fd05f10a5350 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 10 Feb 2023 12:11:12 -0700 Subject: [PATCH 30/35] typing and linting --- .../test_contract_method_abi_decoding.py | 2 -- web3/_utils/abi.py | 6 +++++- web3/_utils/contracts.py | 19 +++++++++++++------ web3/contract/base_contract.py | 3 ++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/core/contracts/test_contract_method_abi_decoding.py b/tests/core/contracts/test_contract_method_abi_decoding.py index 66af21d60e..bc61dc6579 100644 --- a/tests/core/contracts/test_contract_method_abi_decoding.py +++ b/tests/core/contracts/test_contract_method_abi_decoding.py @@ -107,8 +107,6 @@ def test_contract_abi_decoding(w3, abi, data, method, expected): contract = w3.eth.contract(abi=abi) func, params = contract.decode_function_input(data) assert func.fn_name == method - # if method == "liquidate": - # breakpoint() assert params == expected reinvoke_func = contract.functions[func.fn_name](**params) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 8a37297050..178b7dfe75 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -910,7 +910,11 @@ def build_strict_registry() -> ABIRegistry: def named_tree( - abi: Sequence[ABIFunctionParams], data: Tuple[Any, ...] + abi: Union[ + Sequence[ABIFunctionParams], + Iterable[Union[ABIFunction, ABIEvent, Dict[TypeStr, Any]]], + ], + data: Tuple[Any, ...], ) -> Dict[str, Any]: """ Convert function inputs/outputs or event data tuple to dict with names from ABI. diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index a516cae624..9aad3d3cdd 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -2,6 +2,7 @@ from typing import ( TYPE_CHECKING, Any, + Callable, Dict, Optional, Sequence, @@ -14,9 +15,13 @@ from eth_abi.codec import ( ABICodec, ) +from eth_abi.registry import ( + registry as default_registry, +) from eth_typing import ( ChecksumAddress, HexStr, + TypeStr, ) from eth_utils import ( add_0x_prefix, @@ -313,15 +318,17 @@ def encode_transaction_data( return add_0x_prefix(encode_abi(w3, fn_abi, fn_arguments, fn_selector)) -from eth_abi.registry import ( - registry as default_registry, -) -def decode_transaction_data(fn_abi: ABIFunction, data, normalizers=None) -> Dict[str, Any]: - data = HexBytes(data) +def decode_transaction_data( + fn_abi: ABIFunction, + data: HexStr, + normalizers: Sequence[Callable[[TypeStr, Any], Tuple[TypeStr, Any]]] = None, +) -> Dict[str, Any]: + # TODO figure out typing + data = HexBytes(data) # type: ignore types = get_abi_input_types(fn_abi) abi_codec = ABICodec(default_registry) - decoded = abi_codec.decode(types=types, data=data[4:]) + decoded = abi_codec.decode(types, HexBytes(data[4:])) if normalizers: decoded = map_abi_data(normalizers, types, decoded) return named_tree(fn_abi["inputs"], decoded) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index 0315f978b0..e92904fb7f 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -251,7 +251,8 @@ def callable_check(fn_abi: ABIFunction) -> bool: def decode_function_input( self, data: HexStr ) -> Tuple["BaseContractFunction", Dict[str, Any]]: - data = HexBytes(data) + # TODO figure out typing + data = HexBytes(data) # type: ignore func = self.get_function_by_selector(data[:4]) arguments = decode_transaction_data( func.abi, data, normalizers=BASE_RETURN_NORMALIZERS From a34923ee6ecb24705ade718705391eb965d8fc44 Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 10 Feb 2023 12:26:57 -0700 Subject: [PATCH 31/35] test updates --- tests/core/contracts/test_extracting_event_data.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/core/contracts/test_extracting_event_data.py b/tests/core/contracts/test_extracting_event_data.py index 46239be82c..07e113dbf4 100644 --- a/tests/core/contracts/test_extracting_event_data.py +++ b/tests/core/contracts/test_extracting_event_data.py @@ -530,7 +530,7 @@ def test_argument_extraction_strict_bytes_types( "logStruct", "LogStructArgs", [1, (2, 3, (4,))], - {"arg0": 1, "arg1": (2, 3, (4,))}, + {"arg0": 1, "arg1": {"a": 2, "b": 3, "nested": {"c": 4}}}, "The event signature did not match the provided ABI", True, ), @@ -538,7 +538,7 @@ def test_argument_extraction_strict_bytes_types( "logStruct", "LogStructArgs", [1, (2, 3, (4,))], - {"arg0": 1, "arg1": (2, 3, (4,))}, + {"arg0": 1, "arg1": {"a": 2, "b": 3, "nested": {"c": 4}}}, "The event signature did not match the provided ABI", False, ), @@ -793,7 +793,7 @@ def test_event_rich_log( "logStruct", "LogStructArgs", [1, (2, 3, (4,))], - {"arg0": 1, "arg1": (2, 3, (4,))}, + {"arg0": 1, "arg1": {"a": 2, "b": 3, "nested": {"c": 4}}}, "The event signature did not match the provided ABI", True, ), @@ -801,7 +801,7 @@ def test_event_rich_log( "logStruct", "LogStructArgs", [1, (2, 3, (4,))], - {"arg0": 1, "arg1": (2, 3, (4,))}, + {"arg0": 1, "arg1": {"a": 2, "b": 3, "nested": {"c": 4}}}, "The event signature did not match the provided ABI", False, ), From 8a443af24bd76c7a5613f805611feb2739976dab Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 10 Feb 2023 12:43:00 -0700 Subject: [PATCH 32/35] typing --- web3/_utils/contracts.py | 2 +- web3/contract/base_contract.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 9aad3d3cdd..2d1b7a2b48 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -324,7 +324,7 @@ def decode_transaction_data( data: HexStr, normalizers: Sequence[Callable[[TypeStr, Any], Tuple[TypeStr, Any]]] = None, ) -> Dict[str, Any]: - # TODO figure out typing + # type ignored b/c expects data arg to be HexBytes data = HexBytes(data) # type: ignore types = get_abi_input_types(fn_abi) abi_codec = ABICodec(default_registry) diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index e92904fb7f..37a641193b 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -251,7 +251,7 @@ def callable_check(fn_abi: ABIFunction) -> bool: def decode_function_input( self, data: HexStr ) -> Tuple["BaseContractFunction", Dict[str, Any]]: - # TODO figure out typing + # type ignored b/c expects data arg to be HexBytes data = HexBytes(data) # type: ignore func = self.get_function_by_selector(data[:4]) arguments = decode_transaction_data( From b01cd33d89453dfcaf0c7ec71c2afa472a2a779e Mon Sep 17 00:00:00 2001 From: pacrob Date: Fri, 10 Feb 2023 17:05:41 -0700 Subject: [PATCH 33/35] clean up typing per comments --- web3/_utils/abi.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 178b7dfe75..afd4e24531 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -909,12 +909,10 @@ def build_strict_registry() -> ABIRegistry: return registry +# Sequence[ABIFunctionParams], def named_tree( - abi: Union[ - Sequence[ABIFunctionParams], - Iterable[Union[ABIFunction, ABIEvent, Dict[TypeStr, Any]]], - ], - data: Tuple[Any, ...], + abi: Iterable[Union[ABIFunctionParams, ABIFunction, ABIEvent, Dict[TypeStr, Any]]], + data: Iterable[Tuple[Any, ...]], ) -> Dict[str, Any]: """ Convert function inputs/outputs or event data tuple to dict with names from ABI. @@ -926,8 +924,7 @@ def named_tree( def named_subtree( - # TODO make this a better type - abi: Any, + abi: Union[ABIFunctionParams, ABIFunction, ABIEvent, Dict[TypeStr, Any]], data: Tuple[Any, ...], ) -> Union[Dict[str, Any], Tuple[Any, ...], Any]: abi_type = parse(collapse_if_tuple(dict(abi))) @@ -938,7 +935,8 @@ def named_subtree( items = [named_subtree(item_abi, item) for item in data] return items - if isinstance(abi_type, TupleType): + elif isinstance(abi_type, TupleType): + abi = cast(ABIFunctionParams, abi) names = [item["name"] for item in abi["components"]] items = [named_subtree(*item) for item in zip(abi["components"], data)] From d664e6c92158ca3f584d531c74302b78f87cab85 Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 13 Feb 2023 12:22:29 -0700 Subject: [PATCH 34/35] typing in abi.py --- web3/_utils/abi.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index afd4e24531..22687de849 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -909,7 +909,6 @@ def build_strict_registry() -> ABIRegistry: return registry -# Sequence[ABIFunctionParams], def named_tree( abi: Iterable[Union[ABIFunctionParams, ABIFunction, ABIEvent, Dict[TypeStr, Any]]], data: Iterable[Tuple[Any, ...]], @@ -918,27 +917,27 @@ def named_tree( Convert function inputs/outputs or event data tuple to dict with names from ABI. """ names = [item["name"] for item in abi] - items = [named_subtree(*item) for item in zip(abi, data)] + items = [_named_subtree(*item) for item in zip(abi, data)] return dict(zip(names, items)) -def named_subtree( +def _named_subtree( abi: Union[ABIFunctionParams, ABIFunction, ABIEvent, Dict[TypeStr, Any]], data: Tuple[Any, ...], -) -> Union[Dict[str, Any], Tuple[Any, ...], Any]: +) -> Union[Dict[str, Any], Tuple[Any, ...], List[Any]]: abi_type = parse(collapse_if_tuple(dict(abi))) if abi_type.is_array: item_type = abi_type.item_type.to_type_str() item_abi = {**abi, "type": item_type, "name": ""} - items = [named_subtree(item_abi, item) for item in data] + items = [_named_subtree(item_abi, item) for item in data] return items elif isinstance(abi_type, TupleType): abi = cast(ABIFunctionParams, abi) names = [item["name"] for item in abi["components"]] - items = [named_subtree(*item) for item in zip(abi["components"], data)] + items = [_named_subtree(*item) for item in zip(abi["components"], data)] if len(names) == len(data): return dict(zip(names, items)) From f555e688e70657b24e561348fa8dd031393288c3 Mon Sep 17 00:00:00 2001 From: pacrob Date: Mon, 13 Feb 2023 12:46:34 -0700 Subject: [PATCH 35/35] lowercase web3 please --- docs/web3.contract.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/web3.contract.rst b/docs/web3.contract.rst index fc63e68a45..a6531ae99e 100644 --- a/docs/web3.contract.rst +++ b/docs/web3.contract.rst @@ -155,7 +155,7 @@ Each Contract Factory exposes the following properties. value will be a Python `NamedTuple` of the class `ABIDecodedNamedTuple`. NamedTuples have some restrictions regarding field names. - Web3.py sets `NamedTuple`'s `rename=True`, so disallowed field names may be + web3.py sets `NamedTuple`'s `rename=True`, so disallowed field names may be different than expected. See the [Python docs](https://docs.python.org/3/library/collections.html#collections.namedtuple) for more information.