make it work

This commit is contained in:
Piper Merriam 2016-09-05 16:19:17 -06:00
parent c9e83cd828
commit e90aed0e43
5 changed files with 119 additions and 77 deletions

View File

@ -1,6 +1,6 @@
import pytest
from web3.contract import (
from web3.utils.abi import (
merge_args_and_kwargs,
)
@ -19,6 +19,15 @@ FUNCTION_ABI = {
}
NO_INPUTS_FUNCTION_ABI = {
"constant": False
,"inputs": [],
"name": "testFn",
"outputs": [],
"type":"function",
}
@pytest.mark.parametrize(
'args,kwargs,expected_args',
(
@ -32,3 +41,8 @@ FUNCTION_ABI = {
def test_merging_of_args_and_kwargs(args, kwargs, expected_args):
actual_args = merge_args_and_kwargs(FUNCTION_ABI, args, kwargs)
assert actual_args == expected_args
def test_merging_of_args_and_kwargs_with_no_inputs():
actual = merge_args_and_kwargs(NO_INPUTS_FUNCTION_ABI, tuple(), {})
assert actual == tuple()

View File

@ -47,8 +47,16 @@ def test_call_with_one_argument(math_contract):
assert result == 21
def test_call_with_multiple_arguments(math_contract):
result = math_contract.call().add(9, 7)
@pytest.mark.parametrize(
'call_args,call_kwargs',
(
((9, 7), {}),
((9,), {'b': 7}),
(tuple(), {'a': 9, 'b': 7}),
),
)
def test_call_with_multiple_arguments(math_contract, call_args, call_kwargs):
result = math_contract.call().add(*call_args, **call_kwargs)
assert result == 16

View File

@ -41,6 +41,13 @@ def test_transacting_with_contract_no_arguments(web3_tester, math_contract):
assert final_value - initial_value == 1
@pytest.mark.parametrize(
'transact_args,transact_kwargs',
(
((5,), {}),
(tuple(), {'amt': 5}),
),
)
def test_transacting_with_contract_with_arguments(web3_tester, math_contract):
initial_value = math_contract.call().counter()

View File

@ -1,7 +1,6 @@
"""Interaction with smart contracts over Web3 connector.
"""
import itertools
import functools
from eth_abi import (
@ -35,6 +34,7 @@ from web3.utils.abi import (
get_constructor_abi,
check_if_arguments_can_be_encoded,
function_abi_to_4byte_selector,
merge_args_and_kwargs,
)
from web3.utils.decorators import (
combomethod,
@ -274,16 +274,17 @@ class Contract(object):
# ABI Helpers
#
@classmethod
def find_matching_fn_abi(cls, fn_name=None, arguments=None):
def find_matching_fn_abi(cls, fn_name=None, args=None, kwargs=None):
filters = []
if fn_name:
filters.append(functools.partial(filter_by_name, fn_name))
if arguments is not None:
if args is not None and kwargs is not None:
num_arguments = len(args) + len(kwargs)
filters.extend([
functools.partial(filter_by_argument_count, arguments),
functools.partial(filter_by_encodability, arguments),
functools.partial(filter_by_argument_count, num_arguments),
functools.partial(filter_by_encodability, args, kwargs),
])
function_candidates = filter_by_type('function', cls.abi)
@ -370,8 +371,9 @@ class Contract(object):
)
is_encodable = check_if_arguments_can_be_encoded(
get_abi_input_types(constructor),
constructor,
arguments,
{},
)
if not is_encodable:
raise ValueError("Unable to encode provided arguments.")
@ -620,10 +622,11 @@ class Contract(object):
@coerce_return_to_text
def call_contract_function(contract=None,
function_name=None,
transaction=None,
*arguments):
def call_contract_function(contract,
function_name,
transaction,
*args,
**kwargs):
"""Calls a contract constant or function.
The function must not have state changing effects.
@ -643,12 +646,11 @@ def call_contract_function(contract=None,
else:
call_transaction = dict(**transaction)
if not arguments:
arguments = []
function_abi = contract.find_matching_fn_abi(function_name, arguments)
function_abi = contract.find_matching_fn_abi(function_name, args, kwargs)
function_selector = function_abi_to_4byte_selector(function_abi)
arguments = merge_args_and_kwargs(function_abi, args, kwargs)
call_transaction['data'] = contract.encodeABI(
function_name,
arguments,
@ -674,7 +676,8 @@ def call_contract_function(contract=None,
def transact_with_contract_function(contract=None,
function_name=None,
transaction=None,
*arguments):
*args,
**kwargs):
"""Transacts with a contract.
Sends in a transaction that interacts with the contract.
@ -737,12 +740,11 @@ def transact_with_contract_function(contract=None,
else:
transact_transaction = dict(**transaction)
if not arguments:
arguments = []
function_abi = contract.find_matching_fn_abi(function_name, arguments)
function_abi = contract.find_matching_fn_abi(function_name, args, kwargs)
function_selector = function_abi_to_4byte_selector(function_abi)
arguments = merge_args_and_kwargs(function_abi, args, kwargs)
transact_transaction['data'] = contract.encodeABI(
function_name,
arguments,
@ -759,7 +761,8 @@ def transact_with_contract_function(contract=None,
def estimate_gas_for_function(contract=None,
function_name=None,
transaction=None,
*arguments):
*args,
**kwargs):
"""Estimates gas cost a function call would take.
Don't call this directly, instead use :meth:`Contract.estimateGas`
@ -770,12 +773,11 @@ def estimate_gas_for_function(contract=None,
else:
estimate_transaction = dict(**transaction)
if not arguments:
arguments = []
function_abi = contract.find_matching_fn_abi(function_name, arguments)
function_abi = contract.find_matching_fn_abi(function_name, args, kwargs)
function_selector = function_abi_to_4byte_selector(function_abi)
arguments = merge_args_and_kwargs(function_abi, args, kwargs)
estimate_transaction['data'] = contract.encodeABI(
function_name,
arguments,
@ -843,44 +845,3 @@ def construct_contract_class(web3, abi, code=None,
'source': source,
}
return type('Contract', (Contract,), _dict)
def merge_args_and_kwargs(function_abi, args, kwargs):
if len(args) + len(kwargs) != len(function_abi['inputs']):
raise TypeError(
"Incorrect argument count. Expected '{0}'. Got '{1}'".format(
len(function_abi['input']),
len(args) + len(kwargs),
)
)
args_as_kwargs = {
arg_abi['name']: arg
for arg_abi, arg in zip(function_abi['inputs'], args)
}
duplicate_keys = set(args_as_kwargs).intersection(kwargs.keys())
if duplicate_keys:
raise TypeError(
"{fn_name}() got multiple values for argument(s) '{dups}'".format(
fn_name=function_abi['name'],
dups=', '.join(duplicate_keys),
)
)
sorted_arg_names = [arg_abi['name'] for arg_abi in function_abi['inputs']]
unknown_kwargs = {key for key in kwargs.keys() if key not in sorted_arg_names}
if unknown_kwargs:
raise TypeError(
"{fn_name}() got unexpected keyword argument(s) '{dups}'".format(
fn_name=function_abi['name'],
dups=', '.join(unknown_kwargs),
)
)
sorted_args = list(zip(
*sorted(
itertools.chain(kwargs.items(), args_as_kwargs.items()),
key=lambda kv: sorted_arg_names.index(kv[0])
)
))
return sorted_args[1]

View File

@ -53,12 +53,12 @@ def exclude_indexed_event_inputs(event_abi):
return [arg for arg in event_abi['inputs'] if arg['indexed'] is False]
def filter_by_argument_count(arguments, contract_abi):
def filter_by_argument_count(num_arguments, contract_abi):
return [
abi
for abi
in contract_abi
if len(abi['inputs']) == len(arguments)
if len(abi['inputs']) == num_arguments
]
@ -122,25 +122,77 @@ def is_encodable(_type, value):
raise ValueError("Unsupported type")
def filter_by_encodability(arguments, contract_abi):
def filter_by_encodability(args, kwargs, contract_abi):
return [
abi
for abi
function_abi
for function_abi
in contract_abi
if check_if_arguments_can_be_encoded(get_abi_input_types(abi), arguments)
if check_if_arguments_can_be_encoded(function_abi, args, kwargs)
]
@coerce_args_to_bytes
def check_if_arguments_can_be_encoded(types, arguments):
if len(types) != len(arguments):
raise ValueError("Length mismatch between types and arguments")
def check_if_arguments_can_be_encoded(function_abi, args, kwargs):
try:
arguments = merge_args_and_kwargs(function_abi, args, kwargs)
except TypeError:
return False
if len(function_abi['inputs']) != len(arguments):
return False
types = get_abi_input_types(function_abi)
return all(
is_encodable(_type, arg)
for _type, arg in zip(types, arguments)
)
def merge_args_and_kwargs(function_abi, args, kwargs):
if len(args) + len(kwargs) != len(function_abi['inputs']):
raise TypeError(
"Incorrect argument count. Expected '{0}'. Got '{1}'".format(
len(function_abi['input']),
len(args) + len(kwargs),
)
)
args_as_kwargs = {
arg_abi['name']: arg
for arg_abi, arg in zip(function_abi['inputs'], args)
}
duplicate_keys = set(args_as_kwargs).intersection(kwargs.keys())
if duplicate_keys:
raise TypeError(
"{fn_name}() got multiple values for argument(s) '{dups}'".format(
fn_name=function_abi['name'],
dups=', '.join(duplicate_keys),
)
)
sorted_arg_names = [arg_abi['name'] for arg_abi in function_abi['inputs']]
unknown_kwargs = {key for key in kwargs.keys() if key not in sorted_arg_names}
if unknown_kwargs:
raise TypeError(
"{fn_name}() got unexpected keyword argument(s) '{dups}'".format(
fn_name=function_abi['name'],
dups=', '.join(unknown_kwargs),
)
)
sorted_args = list(zip(
*sorted(
itertools.chain(kwargs.items(), args_as_kwargs.items()),
key=lambda kv: sorted_arg_names.index(kv[0])
)
))
if sorted_args:
return sorted_args[1]
else:
return tuple()
def get_constructor_abi(contract_abi):
candidates = [
abi for abi in contract_abi if abi['type'] == 'constructor'