-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
+639
−28
Closed
Naive tuple support #1147
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b47e48c
Naive support for tuples
feuGeneA 9522b9a
Exercise doctests in _utils.abi
feuGeneA 2b8d47e
Renamed some tests for consistency
feuGeneA 3549fde
Test passing a tuple into a contract call
feuGeneA f5173e8
support arrays of tuples
feuGeneA bf0a2fd
Merge branch 'master' into feature/naive-tuple-support
feuGeneA File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note below how the array-type ABI values have |
||
|
||
if data_type is None: | ||
return ABITypedData([None, data_value]) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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: