Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Naive tuple support #1147

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions tests/core/contracts/conftest.py
Original file line number Diff line number Diff line change
@@ -590,6 +590,163 @@ def CallerTesterContract(web3, CALLER_TESTER_CONTRACT):
return web3.eth.contract(**CALLER_TESTER_CONTRACT)


CONTRACT_TUPLE_SOURCE = """
pragma experimental ABIEncoderV2;
contract Tuple {
struct Struct {
int anInt;
bool aBool;
address anAddress;
}
function methodTakingStruct(Struct memory m)
public
pure
returns (Struct memory)
{
return m;
}
function methodTakingArrayOfStructs(Struct[] memory m)
public
pure
returns (Struct[] memory)
{
return m;
}
}"""

CONTRACT_TUPLE_CODE = "608060405234801561001057600080fd5b50610571806100206000396000f3fe608060405260043610610046576000357c0100000000000000000000000000000000000000000000000000000000900480635442981a1461004b578063d0723aff14610088575b600080fd5b34801561005757600080fd5b50610072600480360361006d919081019061029a565b6100c5565b60405161007f9190610410565b60405180910390f35b34801561009457600080fd5b506100af60048036036100aa91908101906102db565b6100cf565b6040516100bc9190610432565b60405180910390f35b6060819050919050565b6100d76100df565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b6000610125823561050f565b905092915050565b600082601f830112151561014057600080fd5b813561015361014e8261047a565b61044d565b9150818183526020840193506020810190508385606084028201111561017857600080fd5b60005b838110156101a8578161018e88826101da565b84526020840193506060830192505060018101905061017b565b5050505092915050565b60006101be8235610521565b905092915050565b60006101d2823561052d565b905092915050565b6000606082840312156101ec57600080fd5b6101f6606061044d565b90506000610206848285016101c6565b600083015250602061021a848285016101b2565b602083015250604061022e84828501610119565b60408301525092915050565b60006060828403121561024c57600080fd5b610256606061044d565b90506000610266848285016101c6565b600083015250602061027a848285016101b2565b602083015250604061028e84828501610119565b60408301525092915050565b6000602082840312156102ac57600080fd5b600082013567ffffffffffffffff8111156102c657600080fd5b6102d28482850161012d565b91505092915050565b6000606082840312156102ed57600080fd5b60006102fb8482850161023a565b91505092915050565b61030d816104c7565b82525050565b600061031e826104af565b808452602084019350610330836104a2565b60005b82811015610362576103468683516103ce565b61034f826104ba565b9150606086019550600181019050610333565b50849250505092915050565b610377816104d9565b82525050565b610386816104e5565b82525050565b6060820160008201516103a2600085018261037d565b5060208201516103b5602085018261036e565b5060408201516103c86040850182610304565b50505050565b6060820160008201516103e4600085018261037d565b5060208201516103f7602085018261036e565b50604082015161040a6040850182610304565b50505050565b6000602082019050818103600083015261042a8184610313565b905092915050565b6000606082019050610447600083018461038c565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561047057600080fd5b8060405250919050565b600067ffffffffffffffff82111561049157600080fd5b602082029050602081019050919050565b6000602082019050919050565b600081519050919050565b6000602082019050919050565b60006104d2826104ef565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061051a826104ef565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a7230582016790dbe833dd3fd348c5d67805f5fae4b7caa58b6f6cd2e1e146e725d1b09506c6578706572696d656e74616cf50037" # noqa: E501

CONTRACT_TUPLE_RUNTIME = "608060405260043610610046576000357c0100000000000000000000000000000000000000000000000000000000900480635442981a1461004b578063d0723aff14610088575b600080fd5b34801561005757600080fd5b50610072600480360361006d919081019061029a565b6100c5565b60405161007f9190610410565b60405180910390f35b34801561009457600080fd5b506100af60048036036100aa91908101906102db565b6100cf565b6040516100bc9190610432565b60405180910390f35b6060819050919050565b6100d76100df565b819050919050565b60606040519081016040528060008152602001600015158152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b6000610125823561050f565b905092915050565b600082601f830112151561014057600080fd5b813561015361014e8261047a565b61044d565b9150818183526020840193506020810190508385606084028201111561017857600080fd5b60005b838110156101a8578161018e88826101da565b84526020840193506060830192505060018101905061017b565b5050505092915050565b60006101be8235610521565b905092915050565b60006101d2823561052d565b905092915050565b6000606082840312156101ec57600080fd5b6101f6606061044d565b90506000610206848285016101c6565b600083015250602061021a848285016101b2565b602083015250604061022e84828501610119565b60408301525092915050565b60006060828403121561024c57600080fd5b610256606061044d565b90506000610266848285016101c6565b600083015250602061027a848285016101b2565b602083015250604061028e84828501610119565b60408301525092915050565b6000602082840312156102ac57600080fd5b600082013567ffffffffffffffff8111156102c657600080fd5b6102d28482850161012d565b91505092915050565b6000606082840312156102ed57600080fd5b60006102fb8482850161023a565b91505092915050565b61030d816104c7565b82525050565b600061031e826104af565b808452602084019350610330836104a2565b60005b82811015610362576103468683516103ce565b61034f826104ba565b9150606086019550600181019050610333565b50849250505092915050565b610377816104d9565b82525050565b610386816104e5565b82525050565b6060820160008201516103a2600085018261037d565b5060208201516103b5602085018261036e565b5060408201516103c86040850182610304565b50505050565b6060820160008201516103e4600085018261037d565b5060208201516103f7602085018261036e565b50604082015161040a6040850182610304565b50505050565b6000602082019050818103600083015261042a8184610313565b905092915050565b6000606082019050610447600083018461038c565b92915050565b6000604051905081810181811067ffffffffffffffff8211171561047057600080fd5b8060405250919050565b600067ffffffffffffffff82111561049157600080fd5b602082029050602081019050919050565b6000602082019050919050565b600081519050919050565b6000602082019050919050565b60006104d2826104ef565b9050919050565b60008115159050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061051a826104ef565b9050919050565b60008115159050919050565b600081905091905056fea265627a7a7230582016790dbe833dd3fd348c5d67805f5fae4b7caa58b6f6cd2e1e146e725d1b09506c6578706572696d656e74616cf50037" # noqa: E501

CONTRACT_TUPLE_ABI = json.loads("""
[
{
"constant": true,
"inputs": [
{
"components": [
{
"name": "anInt",
"type": "int256"
},
{
"name": "aBool",
"type": "bool"
},
{
"name": "anAddress",
"type": "address"
}
],
"name": "m",
"type": "tuple[]"
}
],
"name": "methodTakingArrayOfStructs",
"outputs": [
{
"components": [
{
"name": "anInt",
"type": "int256"
},
{
"name": "aBool",
"type": "bool"
},
{
"name": "anAddress",
"type": "address"
}
],
"name": "",
"type": "tuple[]"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{
"name": "anInt",
"type": "int256"
},
{
"name": "aBool",
"type": "bool"
},
{
"name": "anAddress",
"type": "address"
}
],
"name": "m",
"type": "tuple"
}
],
"name": "methodTakingStruct",
"outputs": [
{
"components": [
{
"name": "anInt",
"type": "int256"
},
{
"name": "aBool",
"type": "bool"
},
{
"name": "anAddress",
"type": "address"
}
],
"name": "",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
}
]""")


@pytest.fixture()
def TUPLE_CODE():
return CONTRACT_TUPLE_CODE


@pytest.fixture()
def TUPLE_RUNTIME():
return CONTRACT_TUPLE_RUNTIME


@pytest.fixture()
def TUPLE_ABI():
return CONTRACT_TUPLE_ABI


@pytest.fixture()
def TUPLE_CONTRACT(TUPLE_CODE, TUPLE_RUNTIME, TUPLE_ABI):
return {
'bytecode': TUPLE_CODE,
'bytecode_runtime': TUPLE_RUNTIME,
'abi': TUPLE_ABI,
}


@pytest.fixture()
def TupleContract(web3, TUPLE_CONTRACT):
return web3.eth.contract(**TUPLE_CONTRACT)


class LogFunctions:
LogAnonymous = 0
LogNoArguments = 1
41 changes: 41 additions & 0 deletions tests/core/contracts/test_contract_call_interface.py
Original file line number Diff line number Diff line change
@@ -134,6 +134,11 @@ def fallback_function_contract(web3, FallballFunctionContract, address_conversio
return deploy(web3, FallballFunctionContract, address_conversion_func)


@pytest.fixture()
def tuple_contract(web3, TupleContract, address_conversion_func):
return deploy(web3, TupleContract, address_conversion_func)


def test_invalid_address_in_deploy_arg(web3, WithConstructorAddressArgumentsContract):
with pytest.raises(InvalidAddress):
WithConstructorAddressArgumentsContract.constructor(
@@ -611,3 +616,39 @@ def test_invalid_fixed_value_reflections(web3, fixed_reflection_contract, functi
contract_func = fixed_reflection_contract.functions[function]
with pytest.raises(ValidationError, match=error):
contract_func(value).call({'gas': 420000})


@pytest.mark.parametrize(
'method_input, expected',
(
(
{'anInt': 0, 'aBool': True, 'anAddress': '0x' + 'f' * 40},
(0, True, '0x' + 'f' * 40)
),
(
(0, True, '0x' + 'f' * 40),
(0, True, '0x' + 'f' * 40),
),
)
)
def test_call_tuple_contract_struct(tuple_contract, method_input, expected):
result = tuple_contract.functions.methodTakingStruct(method_input).call()
assert result == expected


@pytest.mark.parametrize(
'method_input, expected',
(
(
[{'anInt': 0, 'aBool': True, 'anAddress': '0x' + 'f' * 40}],
((0, True, '0x' + 'f' * 40),)
),
(
[(0, True, '0x' + 'f' * 40)],
((0, True, '0x' + 'f' * 40),)
),
)
)
def test_call_tuple_contract_struct_array(tuple_contract, method_input, expected):
result = tuple_contract.functions.methodTakingArrayOfStructs(method_input).call()
assert result == expected
39 changes: 39 additions & 0 deletions tests/core/contracts/test_contract_util_functions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest

from web3.contract import (
find_matching_fn_abi,
parse_block_identifier_int,
)

@@ -11,3 +14,39 @@
def test_parse_block_identifier_int(web3):
last_num = web3.eth.getBlock('latest').number
assert 0 == parse_block_identifier_int(web3, -1 - last_num)


@pytest.mark.parametrize(
'contract_abi, fn_name, args, kwargs, expected',
(
(
[
{
'inputs': [],
'type': 'function',
'name': 'a',
},
{
'inputs': [{'type': 'bytes32'}],
'type': 'function',
'name': 'a',
},
{
'inputs': [{'type': 'uint256'}],
'type': 'function',
'name': 'a',
},
],
'a',
[1],
None,
{
'inputs': [{'type': 'uint256'}],
'type': 'function',
'name': 'a',
},
),
),
)
def test_find_matching_fn_abi(fn_name, contract_abi, args, kwargs, expected):
assert expected == find_matching_fn_abi(contract_abi, fn_name, args, kwargs)
116 changes: 115 additions & 1 deletion tests/core/utilities/test_abi.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@

import doctest
import pytest

import web3._utils.abi
from web3._utils.abi import (
ABITypedData,
abi_data_tree,
data_tree_map,
get_abi_inputs,
map_abi_data,
)
from web3._utils.normalizers import (
BASE_RETURN_NORMALIZERS,
addresses_checksummed,
)


@@ -29,9 +34,45 @@ def test_abi_data_tree(types, data, expected):
assert abi_data_tree(types, data) == expected


@pytest.mark.parametrize(
'func, data_tree, expected',
[
(
addresses_checksummed,
[
ABITypedData(
[
'address',
b'\xf2\xe2F\xbbv\xdf\x87l\xef\x8b8\xae\x84\x13\x0fOU\xde9[',
]
),
ABITypedData([None, 'latest'])
],
[
ABITypedData(
[
'address',
'0xF2E246BB76DF876Cef8b38ae84130F4F55De395b',
]
),
ABITypedData([None, 'latest'])
]
)
],
)
def test_data_tree_map(func, data_tree, expected):
assert data_tree_map(func, data_tree) == expected


@pytest.mark.parametrize(
'types, data, funcs, expected',
[
( # like web3._utils.rpc_abi.RPC_ABIS['eth_getCode']
['address', None],
Copy link
Collaborator

@carver carver Jan 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has come up a few times, so let's all be on the lookout for it: recursive structures must be handled. For one example, what happens when we try to run a map function over ABI data that has an array inside a tuple?

Something like:

        (
            ['(string,address[])'],
            [(b'a string', [b'\xf2\xe2F\xbbv\xdf\x87l\xef\x8b8\xae\x84\x13\x0fOU\xde9['],
            [addresses_checksummed, abi_string_to_text],
            [('a string', ['0xF2E246BB76DF876Cef8b38ae84130F4F55De395b'])],
        ),

[b'\xf2\xe2F\xbbv\xdf\x87l\xef\x8b8\xae\x84\x13\x0fOU\xde9[', 'latest'],
BASE_RETURN_NORMALIZERS,
['0xF2E246BB76DF876Cef8b38ae84130F4F55De395b', 'latest'],
),
(
["bool[2]", "int256"],
[[True, False], 9876543210],
@@ -57,3 +98,76 @@ def test_abi_data_tree(types, data, expected):
)
def test_map_abi_data(types, data, funcs, expected):
assert map_abi_data(funcs, types, data) == expected


FN_ABI = {
'inputs': [
{
'components': [
{'name': 'anAddress', 'type': 'address'},
{'name': 'anInt', 'type': 'uint256'},
{'name': 'someBytes', 'type': 'bytes'},
],
'type': 'tuple'
},
],
'type': 'function'
}

FN_ARG_VALUES_AS_DICT = (
{
'someBytes': b'0000',
'anInt': 0,
'anAddress': '0x' + '0' * 40,
},
)

FN_ARG_VALUES_AS_TUPLE = (
(
'0x' + '0' * 40,
0,
b'0000',
),
)


@pytest.mark.parametrize(
'function_abi, arg_values, expected',
[
(
FN_ABI,
FN_ARG_VALUES_AS_DICT,
(
[
'(address,uint256,bytes)'
],
FN_ARG_VALUES_AS_TUPLE,
),
),
(
FN_ABI,
FN_ARG_VALUES_AS_TUPLE,
(
[
'(address,uint256,bytes)'
],
FN_ARG_VALUES_AS_TUPLE,
),
),
(
{'payable': False, 'stateMutability': 'nonpayable', 'type': 'fallback'},
(),
([], ()),
)
]
)
def test_get_abi_inputs(function_abi, arg_values, expected):
assert get_abi_inputs(function_abi, arg_values) == expected


def test_docstrings(capsys):
"""Exercise docstrings in the web3._utils.abi module."""
# disable stdout capture so failed tests will show why they failed
with capsys.disabled():
failure_count, _ = doctest.testmod(web3._utils.abi)
assert failure_count == 0
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@
(['b'], ['func_3', 'func_4']),
)
)
def test_filter_by_arguments_1(argument_names, expected):
def test_filter_by_argument_name(argument_names, expected):
actual_matches = filter_by_argument_name(argument_names, ABI)
function_names = [match['name'] for match in actual_matches]
assert set(function_names) == set(expected)
59 changes: 59 additions & 0 deletions tests/core/utilities/test_abi_filter_by_encodability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest

from web3._utils.abi import (
filter_by_encodability,
)

FN_ABI_ONE_ADDRESS_ARG = {'inputs': [{'name': 'arg', 'type': 'address'}]}

FN_ABI_MIXED_ARGS = {
'inputs': [
{
'components': [
{'name': 'anAddress', 'type': 'address'},
{'name': 'anInt', 'type': 'uint256'},
{'name': 'someBytes', 'type': 'bytes'},
],
'type': 'tuple'
}
],
'type': 'function'
}


@pytest.mark.parametrize(
'arguments,contract_abi,expected_match_count,expected_first_match',
(
(
('0x' + '1' * 40,),
[FN_ABI_ONE_ADDRESS_ARG],
1,
FN_ABI_ONE_ADDRESS_ARG,
),
(
('0xffff'), # not a valid address
[FN_ABI_ONE_ADDRESS_ARG],
0,
None,
),
(
(
{
'anAddress': '0x' + '0' * 40,
'anInt': 1,
'someBytes': b'\x00' * 20,
},
),
[FN_ABI_MIXED_ARGS],
1,
FN_ABI_MIXED_ARGS,
),
)
)
def test_filter_by_encodability(
arguments, contract_abi, expected_match_count, expected_first_match
):
filter_output = filter_by_encodability(arguments, {}, contract_abi)
assert len(filter_output) == expected_match_count
if expected_match_count > 0:
assert filter_output[0] == expected_first_match
Original file line number Diff line number Diff line change
@@ -66,6 +66,6 @@
('does_not_exist', []),
)
)
def test_filter_by_arguments(name, expected):
def test_filter_by_name(name, expected):
actual_matches = filter_by_name(name, ABI)
assert actual_matches == expected
76 changes: 76 additions & 0 deletions tests/core/utilities/test_abi_is_encodable.py
Original file line number Diff line number Diff line change
@@ -70,6 +70,82 @@
((b'anything', 0), '(string,int128)', True),
((b'\x80', 0), '(string,int128)', False),
(['0x' + '00' * 20, 0], '(address,uint256)', True),
(('0x' + '00' * 20, 0), '(address,uint256)', True),
([0], '(address,uint256)', False),
(['0x' + '00' * 20], '(uint256)', False),
([], '(address)', False),
((1, (2, 3), 0), '(uint256,(uint256,uint256),uint256)', True),
((0, (2, 3)), '(uint256,(uint256,uint256))', True),
(((2, 3), 0), '((uint256,uint256),uint256)', True),
((((0,),),), '(((uint256)))', True),
([0, 1, 2, 3], 'uint256[]', True),
([(0, 1), (2, 3)], '(uint256,uint256)[]', True),
([(0, 1, 2), (3, 4, 5)], '(uint256,uint256,uint256)[]', True),
(
[0, ['0x' + '00' * 20, '0x' + '00' * 20], ['0x' + '00' * 20, '0x' + '00' * 20], 5],
'(int,(address,address),(address,address),int)',
True,
),
(
(0, ('0x' + '00' * 20, '0x' + '00' * 20), ('0x' + '00' * 20, '0x' + '00' * 20), 5),
'(int,(address,address),(address,address),int)',
True,
),
(
[
['0x' + '00' * 20, '0x' + '00' * 20],
2,
['0x' + '00' * 20, '0x' + '00' * 20],
5,
['0x' + '00' * 20, '0x' + '00' * 20],
],
'((address,address),int,(address,address),int,(address,address))',
True,
),
(
(
('0x' + '00' * 20, '0x' + '00' * 20),
2,
('0x' + '00' * 20, '0x' + '00' * 20),
5,
('0x' + '00' * 20, '0x' + '00' * 20),
),
'((address,address),int,(address,address),int,(address,address))',
True,
),
(
[0, ['0x' + '00' * 20, [1, 2]], 3],
'(int,(address,(int,int)),int)',
True,
),
(
(0, ('0x' + '00' * 20, (1, 2)), 3),
'(int,(address,(int,int)),int)',
True,
),
(
[
[['0x' + '00' * 20], '0x' + '00' * 20],
1,
['0x' + '00' * 20, '0x' + '00' * 20],
2,
[[3, [4, 5]], '0x' + '00' * 20]
],
'(((address),address),int,(address,address),int,((int,(int,int)),address))',
True,
),
(
(
(('0x' + '00' * 20,), '0x' + '00' * 20),
1,
('0x' + '00' * 20, '0x' + '00' * 20),
2,
((3, (4, 5)), '0x' + '00' * 20)
),
'(((address),address),int,(address,address),int,((int,(int,int)),address))',
True,
),
),
)
def test_is_encodable(value, _type, expected):
172 changes: 147 additions & 25 deletions web3/_utils/abi.py
Original file line number Diff line number Diff line change
@@ -12,17 +12,24 @@
from eth_abi.codec import (
ABICodec,
)
from eth_abi.grammar import (
parse as parse_type_string,
)
from eth_abi.registry import (
BaseEquals,
registry as default_registry,
)
from eth_utils import (
decode_hex,
is_bytes,
is_list_like,
is_text,
to_text,
to_tuple,
)
from eth_utils.abi import (
collapse_if_tuple,
)

from web3._utils.ens import (
is_ens_name,
@@ -60,14 +67,14 @@ def get_abi_input_types(abi):
if 'inputs' not in abi and abi['type'] == 'fallback':
return []
else:
return [arg['type'] for arg in abi['inputs']]
return [collapse_if_tuple(abi_input) for abi_input in abi['inputs']]


def get_abi_output_types(abi):
if abi['type'] == 'fallback':
return []
else:
return [arg['type'] for arg in abi['outputs']]
return [collapse_if_tuple(arg) for arg in abi['outputs']]


def get_abi_input_names(abi):
@@ -123,32 +130,24 @@ def filter_by_argument_name(argument_names, contract_abi):
)
except ImportError:
from eth_abi.grammar import (
parse as parse_type_string,
normalize as normalize_type_string,
TupleType,
)

def process_type(type_str):
normalized_type_str = normalize_type_string(type_str)
abi_type = parse_type_string(normalized_type_str)

if isinstance(abi_type, TupleType):
type_str_repr = repr(type_str)
if type_str != normalized_type_str:
type_str_repr = '{} (normalized to {})'.format(
type_str_repr,
repr(normalized_type_str),
)

raise ValueError(
"Cannot process type {}: tuple types not supported".format(
type_str_repr,
)
)

abi_type.validate()

sub = abi_type.sub
if hasattr(abi_type, 'base'):
base = abi_type.base
else:
base = str(abi_type.item_type)

if hasattr(abi_type, 'sub'):
sub = abi_type.sub
else:
sub = None
if isinstance(sub, tuple):
sub = 'x'.join(map(str, sub))
elif isinstance(sub, int):
@@ -162,7 +161,7 @@ def process_type(type_str):
else:
arrlist = []

return abi_type.base, sub, arrlist
return base, sub, arrlist

def collapse_type(base, sub, arrlist):
return base + str(sub) + ''.join(map(repr, arrlist))
@@ -257,6 +256,114 @@ def filter_by_encodability(args, kwargs, contract_abi):
]


def get_abi_inputs(function_abi, arg_values):
"""Similar to get_abi_input_types(), but gets values too.
Returns a zip of types and their corresponding argument values.
Importantly, looks in `function_abi` for tuples, and for any found, (a)
translates them from the ABI dict representation to the parenthesized type
list representation that's expected by eth_abi, and (b) translates their
corresponding arguments values from the python dict representation to the
tuple representation expected by eth_abi.
>>> get_abi_inputs(
... {
... 'inputs': [
... {
... 'components': [
... {'name': 'anAddress', 'type': 'address'},
... {'name': 'anInt', 'type': 'uint256'},
... {'name': 'someBytes', 'type': 'bytes'}
... ],
... 'name': 'arg',
... 'type': 'tuple'
... }
... ],
... 'type': 'function'
... },
... (
... {
... 'anInt': 12345,
... 'anAddress': '0x0000000000000000000000000000000000000000',
... 'someBytes': b'0000',
... },
... ),
... )
(['(address,uint256,bytes)'], (('0x0000000000000000000000000000000000000000', 12345, b'0000'),))
"""
if "inputs" not in function_abi:
return ([], ())

def collate_tuple_components(components, values):
"""Collates tuple components with their values.
:param components: is an array of ABI components, such as one extracted
from an input element of a function ABI.
:param values: can be any of a list, tuple, or dict. If a dict, key
names must correspond to component names specified in the components
parameter. If a list or array, the order of the elements should
correspond to the order of elements in the components array.
Returns a two-element tuple. The first element is a string comprised
of the parenthesized list of tuple component types. The second element
is a tuple of the values corresponding to the types in the first
element.
>>> collate_tuple_components(
... [
... {'name': 'anAddress', 'type': 'address'},
... {'name': 'anInt', 'type': 'uint256'},
... {'name': 'someBytes', 'type': 'bytes'}
... ],
... (
... {
... 'anInt': 12345,
... 'anAddress': '0x0000000000000000000000000000000000000000',
... 'someBytes': b'0000',
... },
... ),
... )
"""
component_types = []
component_values = []
for component, value in zip(components, values):
component_types.append(component["type"])
if isinstance(values, dict):
component_values.append(values[component["name"]])
elif is_list_like(values):
component_values.append(value)
else:
raise TypeError(
"Unknown value type {} for ABI type 'tuple'"
.format(type(values))
)
return component_types, component_values

types = []
values = tuple()
for abi_input, arg_value in zip(function_abi["inputs"], arg_values):
if abi_input["type"] == "tuple[]":
value_array = []
for arg_arr_elem_val in arg_value:
component_types, component_values = collate_tuple_components(
abi_input["components"], arg_arr_elem_val
)
value_array.append(component_values)
types.append("(" + ",".join(component_types) + ")[]")
values += (value_array,)
elif abi_input["type"] == "tuple":
component_types, component_values = collate_tuple_components(
abi_input["components"], arg_value
)
types.append("(" + ",".join(component_types) + ")")
values += (tuple(component_values),)
else:
types.append(abi_input["type"])
values += (arg_value,)
return types, values


def check_if_arguments_can_be_encoded(function_abi, args, kwargs):
try:
arguments = merge_args_and_kwargs(function_abi, args, kwargs)
@@ -266,7 +373,7 @@ def check_if_arguments_can_be_encoded(function_abi, args, kwargs):
if len(function_abi.get('inputs', [])) != len(arguments):
return False

types = get_abi_input_types(function_abi)
types, arguments = get_abi_inputs(function_abi, arguments)

return all(
is_encodable(_type, arg)
@@ -568,8 +675,8 @@ def abi_data_tree(types, data):
As an example:
>>> abi_data_tree(types=["bool[2]", "uint"], data=[[True, False], 0])
[("bool[2]", [("bool", True), ("bool", False)]), ("uint256", 0)]
"""
[ABITypedData(abi_type='bool[2]', data=[ABITypedData(abi_type='bool', data=True), ABITypedData(abi_type='bool', data=False)]), ABITypedData(abi_type='uint256', data=0)]
""" # noqa: E501 (line too long)
return [
abi_sub_tree(data_type, data_value)
for data_type, data_value
@@ -584,7 +691,13 @@ def data_tree_map(func, data_tree):
receive two args: abi_type, and data
"""
def map_to_typed_data(elements):
if isinstance(elements, ABITypedData) and elements.abi_type is not None:
if (
isinstance(elements, ABITypedData) and elements.abi_type is not None and
not (
isinstance(elements.abi_type, str) and
elements.abi_type[0] == "("
)
):
return ABITypedData(func(*elements))
else:
return elements
@@ -595,9 +708,11 @@ class ABITypedData(namedtuple('ABITypedData', 'abi_type, data')):
"""
This class marks data as having a certain ABI-type.
>>> addr1 = "0x" + "0" * 20
>>> addr2 = "0x" + "f" * 20
>>> a1 = ABITypedData(['address', addr1])
>>> a2 = ABITypedData(['address', addr2])
>>> addrs = ABITypedData(['address[]', [a1, a2])
>>> addrs = ABITypedData(['address[]', [a1, a2]])
You can access the fields using tuple() interface, or with
attributes:
@@ -614,6 +729,13 @@ def __new__(cls, iterable):


def abi_sub_tree(data_type, data_value):
if (
isinstance(data_type, str) and
data_type[0] == "(" and
isinstance(data_value, tuple)
):
return ABITypedData([data_type, data_value])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note below how the array-type ABI values have abi_sub_tree applied recursively? I was expecting something like that for a tuple. Otherwise, I suspect data_tree_map will fail to apply to the types within the tuple (or arrays within the tuple).


if data_type is None:
return ABITypedData([None, data_value])

3 changes: 3 additions & 0 deletions web3/_utils/contracts.py
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
filter_by_name,
filter_by_type,
get_abi_input_types,
get_abi_inputs,
get_fallback_func_abi,
map_abi_data,
merge_args_and_kwargs,
@@ -240,6 +241,8 @@ def get_function_info(fn_name, contract_abi=None, fn_abi=None, args=None, kwargs

fn_arguments = merge_args_and_kwargs(fn_abi, args, kwargs)

_, fn_arguments = get_abi_inputs(fn_abi, fn_arguments)

return fn_abi, fn_selector, fn_arguments