diff --git a/src/ape/managers/converters.py b/src/ape/managers/converters.py index 3859d9fcd2..ae82b7771e 100644 --- a/src/ape/managers/converters.py +++ b/src/ape/managers/converters.py @@ -246,6 +246,8 @@ def _converters(self) -> Dict[Type, List[ConverterAPI]]: Decimal: [], list: [ListTupleConverter()], tuple: [ListTupleConverter()], + bool: [], + str: [], } for plugin_name, (conversion_type, converter_class) in self.plugin_manager.converters: @@ -278,7 +280,7 @@ def is_type(self, value: Any, type: Type) -> bool: else: return isinstance(value, type) - def convert(self, value: Any, type: Type) -> Any: + def convert(self, value: Any, type: Union[Type, Tuple, List]) -> Any: """ Convert the given value to the given type. This method accesses all :class:`~ape.api.convert.ConverterAPI` instances known to @@ -296,7 +298,24 @@ def convert(self, value: Any, type: Type) -> Any: any: The same given value but with the new given type. """ - if type not in self._converters: + if isinstance(value, (list, tuple)) and isinstance(type, tuple): + # We expected to convert a tuple type, so convert each item in the tuple. + # NOTE: We allow values to be a list, just in case it is a list + return [self.convert(v, t) for v, t in zip(value, type)] + + elif isinstance(value, list) and isinstance(type, list) and len(type) == 1: + # We expected to convert an array type(dynamic or static), + # so convert each item in the list. + # NOTE: type for static and dynamic array is a single item + # list containing the type of the array. + return [self.convert(v, type[0]) for v in value] + + elif isinstance(type, (list, tuple)): + raise ConversionError( + f"Value '{value}' must be a list or tuple when given multiple types." + ) + + elif type not in self._converters: options = ", ".join([t.__name__ for t in self._converters]) raise ConversionError(f"Type '{type}' must be one of [{options}].") diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index d49a982276..8ea1f76084 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -23,7 +23,13 @@ from ape.api import BlockAPI, EcosystemAPI, PluginConfig, ReceiptAPI, TransactionAPI from ape.api.networks import LOCAL_NETWORK_NAME, ProxyInfoAPI from ape.contracts.base import ContractCall -from ape.exceptions import ApeException, APINotImplementedError, ContractError, DecodingError +from ape.exceptions import ( + ApeException, + APINotImplementedError, + ContractError, + ConversionError, + DecodingError, +) from ape.logging import logger from ape.types import ( AddressType, @@ -338,6 +344,37 @@ def decode_block(self, data: Dict) -> BlockAPI: return Block.parse_obj(data) + def _python_type_for_abi_type(self, abi_type: ABIType) -> Union[Type, Tuple, List]: + # NOTE: An array can be an array of tuples, so we start with an array check + if str(abi_type.type).endswith("]"): + # remove one layer of the potential onion of array + new_type = "[".join(str(abi_type.type).split("[")[:-1]) + # create a new type with the inner type of array + new_abi_type = ABIType(type=new_type, **abi_type.dict(exclude={"type"})) + # NOTE: type for static and dynamic array is a single item list + # containing the type of the array + return [self._python_type_for_abi_type(new_abi_type)] + + if abi_type.components is not None: + return tuple(self._python_type_for_abi_type(c) for c in abi_type.components) + + if abi_type.type == "address": + return AddressType + + elif abi_type.type == "bool": + return bool + + elif abi_type.type == "string": + return str + + elif "bytes" in abi_type.type: + return bytes + + elif "int" in abi_type.type: + return int + + raise ConversionError(f"Unable to convert '{abi_type}'.") + def encode_calldata(self, abi: Union[ConstructorABI, MethodABI], *args) -> HexBytes: if not abi.inputs: return HexBytes("") @@ -345,7 +382,8 @@ def encode_calldata(self, abi: Union[ConstructorABI, MethodABI], *args) -> HexBy parser = StructParser(abi) arguments = parser.encode_input(args) input_types = [i.canonical_type for i in abi.inputs] - converted_args = self.conversion_manager.convert(arguments, tuple) + python_types = tuple(self._python_type_for_abi_type(i) for i in abi.inputs) + converted_args = self.conversion_manager.convert(arguments, python_types) encoded_calldata = encode(input_types, converted_args) return HexBytes(encoded_calldata) diff --git a/tests/functional/conversion/test_ether.py b/tests/functional/conversion/test_ether.py index b108d3e63a..4473bea787 100644 --- a/tests/functional/conversion/test_ether.py +++ b/tests/functional/conversion/test_ether.py @@ -29,7 +29,8 @@ def test_bad_type(): convert(value="something", type=float) expected = ( - "Type '' must be one of [ChecksumAddress, bytes, int, Decimal, list, tuple]." + "Type '' must be one of " + "[ChecksumAddress, bytes, int, Decimal, list, tuple, bool, str]." ) assert str(err.value) == expected diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index 46380d59a2..ac35340fb2 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -2,7 +2,8 @@ import pytest from eth_typing import HexAddress, HexStr -from hexbytes import HexBytes +from ethpm_types import HexBytes +from ethpm_types.abi import ABIType, MethodABI from ape.api.networks import LOCAL_NETWORK_NAME from ape.types import AddressType @@ -52,6 +53,38 @@ def test_encode_address(ethereum): assert actual == raw_address +def test_encode_calldata(ethereum): + abi = MethodABI( + type="function", + name="callMe", + inputs=[ + ABIType(name="a", type="bytes4"), + ABIType(name="b", type="address"), + ABIType(name="c", type="uint256"), + ABIType(name="d", type="bytes4[]"), + ], + ) + address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + byte_array = ["0x456", "0x678"] + values = ("0x123", address, HexBytes(55), byte_array) + + actual = ethereum.encode_calldata(abi, *values) + expected = HexBytes( + # 0x123 + "0123000000000000000000000000000000000000000000000000000000000000" + # address + "000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045" + # HexBytes(55) + "0000000000000000000000000000000000000000000000000000000000000037" + # byte_array + "0000000000000000000000000000000000000000000000000000000000000080" + "0000000000000000000000000000000000000000000000000000000000000002" + "0456000000000000000000000000000000000000000000000000000000000000" + "0678000000000000000000000000000000000000000000000000000000000000" + ) + assert actual == expected + + def test_block_handles_snake_case_parent_hash(eth_tester_provider, sender, receiver): # Transaction to change parent hash of next block sender.transfer(receiver, "1 gwei")