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

Contract Caller Cleaned up #1227

Merged
merged 30 commits into from
Feb 11, 2019
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
deb435b
First pass on Contract caller
kclowes Jan 30, 2019
7bda172
ContractReader works with parentheses option
kclowes Jan 30, 2019
87225b8
Allow reader to take a transaction_dict
kclowes Jan 30, 2019
ceab93b
Change all 'reader' references to 'caller'
kclowes Jan 30, 2019
abdbd02
Add test to make sure a contract without an ABI doesn't return an error
kclowes Jan 30, 2019
5384fbb
Add deprecation message
kclowes Jan 30, 2019
23603c8
Get rid of ContractMethod, raise error if no ABI
kclowes Jan 30, 2019
07bfbf3
Pass around args in a way that makes tests pass
kclowes Jan 30, 2019
6844adc
Change ImplicitContract deprecation method,
kclowes Jan 30, 2019
0b34a23
Tests passing for ENS
kclowes Jan 30, 2019
f4e34fc
Fix linting
kclowes Jan 30, 2019
a84cc96
ConciseContract tests passing
kclowes Jan 30, 2019
7e65e9c
Allow no ABI until a function is called
kclowes Jan 30, 2019
cbb2a55
Move ContractCaller tests to their own file
kclowes Jan 30, 2019
e35aeb4
Test that block_identifier is set correctly
kclowes Jan 30, 2019
4000174
Test that address gets set correctly
kclowes Jan 30, 2019
ec73ea1
Add block number to CallerTester contract
kclowes Jan 30, 2019
9ef6efe
Add block identifier to CallerTester contract
kclowes Jan 30, 2019
40aa86a
Refactor out none_or_zero_address function
kclowes Jan 30, 2019
0ed10a1
Add documentation to ContractCaller class
kclowes Jan 30, 2019
d0bc7cc
Whitespace
kclowes Jan 30, 2019
93d4501
More whitespace
kclowes Jan 31, 2019
e35362a
Test deprecation methods,
kclowes Jan 31, 2019
1b17a11
ContractCaller docs are tested
kclowes Feb 1, 2019
ca7919f
Sort imports correctly
kclowes Feb 1, 2019
7d551be
Add deprecation warnings to ConciseContract and ImplicitContract in docs
kclowes Feb 1, 2019
adcafa5
Add elif and space in error message
kclowes Feb 1, 2019
e959c9c
fix linting
kclowes Feb 1, 2019
4a6da73
Rename transaction_dict to transaction.
kclowes Feb 6, 2019
da6888e
Add block_identifier example
kclowes Feb 7, 2019
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
30 changes: 30 additions & 0 deletions docs/contracts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -745,3 +745,33 @@ Utils
'_debatingPeriod': 604800,
'_newCurator': True})

ContractCaller
--------------

.. py:class:: ContractCaller
The :py:attr:`Contract.caller` is a shorthand way to call functions in a contract. This class is not to be used directly, but instead through :py:attr:`Contract.caller`.

There are a number of different ways to invoke the :py:attr:`Contract.caller`.

For example:

.. code-block:: python
>>> myContract = web3.eth.contract(address=contract_address, abi=contract_abi)
>>> twentyone = myContract.caller.multiply7(3)
It can also be invoked using parentheses:

.. code-block:: python
>>> myContract = web3.eth.contract(address=contract_address, abi=contract_abi)
>>> twentyone = myContract.caller().multiply7(3)
And a transaction dictionary, with or without the `transaction_dict` keyword. For example:

.. code-block:: python
>>> myContract = web3.eth.contract(address=contract_address, abi=contract_abi)
>>> twentyone = myContract.caller({'from': '0x...'}).multiply7(3)
>>> myContract = web3.eth.contract(address=contract_address, abi=contract_abi)
kclowes marked this conversation as resolved.
Show resolved Hide resolved
>>> twentyone = myContract.caller(transaction_dict={'from': '0x...'}).multiply7(3)
Like :py:class:`ContractFunction`, :py:class:`ContractCaller`
provides methods to interact with contract functions.
Positional and keyword arguments supplied to the contract caller subclass
will be used to find the contract function by signature,
and forwarded to the contract function when applicable.
53 changes: 27 additions & 26 deletions ens/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
init_web3,
is_valid_name,
label_to_hash,
none_or_zero_address,
normal_name_to_hash,
normalize_name,
raw_name_to_hash,
Expand Down Expand Up @@ -90,7 +91,6 @@ def name(self, address):
"""
reversed_domain = address_to_reverse_domain(address)
return self.resolve(reversed_domain, get='name')
reverse = name

@dict_copy
def setup_address(self, name, address=default, transact={}):
Expand All @@ -113,7 +113,7 @@ def setup_address(self, name, address=default, transact={}):
"""
owner = self.setup_owner(name, transact=transact)
self._assert_control(owner, name)
if not address or address == EMPTY_ADDR_HEX:
if none_or_zero_address(address):
address = None
elif address is default:
address = owner
Expand All @@ -127,7 +127,7 @@ def setup_address(self, name, address=default, transact={}):
address = EMPTY_ADDR_HEX
transact['from'] = owner
resolver = self._set_resolver(name, transact=transact)
return resolver.setAddr(raw_name_to_hash(name), address, transact=transact)
return resolver.functions.setAddr(raw_name_to_hash(name), address).transact(transact)

@dict_copy
def setup_name(self, name, address=None, transact={}):
Expand All @@ -150,18 +150,18 @@ def setup_name(self, name, address=None, transact={}):
return self._setup_reverse(None, address, transact=transact)
else:
resolved = self.address(name)
if not address:
if none_or_zero_address(address):
address = resolved
elif resolved and address != resolved:
elif resolved and address != resolved and resolved != EMPTY_ADDR_HEX:
raise AddressMismatch(
"Could not set address %r to point to name, because the name resolves to %r. "
"To change the name for an existing address, call setup_address() first." % (
address, resolved
)
)
if not address:
if none_or_zero_address(address):
address = self.owner(name)
if not address:
if none_or_zero_address(address):
raise UnownedName("claim subdomain using setup_address() first")
if is_binary_address(address):
address = to_checksum_address(address)
Expand All @@ -176,15 +176,18 @@ def resolve(self, name, get='addr'):
normal_name = normalize_name(name)
resolver = self.resolver(normal_name)
if resolver:
lookup_function = getattr(resolver, get)
lookup_function = getattr(resolver.functions, get)
namehash = normal_name_to_hash(normal_name)
return lookup_function(namehash)
address = lookup_function(namehash).call()
if none_or_zero_address(address):
return None
return lookup_function(namehash).call()
else:
return None

def resolver(self, normal_name):
resolver_addr = self.ens.resolver(normal_name_to_hash(normal_name))
if not resolver_addr:
resolver_addr = self.ens.caller.resolver(normal_name_to_hash(normal_name))
if none_or_zero_address(resolver_addr):
return None
return self._resolverContract(address=resolver_addr)

Expand All @@ -204,7 +207,7 @@ def owner(self, name):
:rtype: str
"""
node = raw_name_to_hash(name)
return self.ens.owner(node)
return self.ens.caller.owner(node)

@dict_copy
def setup_owner(self, name, new_owner=default, transact={}):
Expand Down Expand Up @@ -265,36 +268,34 @@ def _first_owner(self, name):
owner = None
unowned = []
pieces = normalize_name(name).split('.')
while pieces and not owner:
while pieces and none_or_zero_address(owner):
name = '.'.join(pieces)
owner = self.owner(name)
if not owner:
if none_or_zero_address(owner):
unowned.append(pieces.pop(0))
return (owner, unowned, name)

@dict_copy
def _claim_ownership(self, owner, unowned, owned, old_owner=None, transact={}):
transact['from'] = old_owner or owner
for label in reversed(unowned):
self.ens.setSubnodeOwner(
self.ens.functions.setSubnodeOwner(
raw_name_to_hash(owned),
label_to_hash(label),
owner,
transact=transact
)
owner
).transact(transact)
owned = "%s.%s" % (label, owned)

@dict_copy
def _set_resolver(self, name, resolver_addr=None, transact={}):
if not resolver_addr:
if none_or_zero_address(resolver_addr):
resolver_addr = self.address('resolver.eth')
namehash = raw_name_to_hash(name)
if self.ens.resolver(namehash) != resolver_addr:
self.ens.setResolver(
if self.ens.caller.resolver(namehash) != resolver_addr:
self.ens.functions.setResolver(
namehash,
resolver_addr,
transact=transact
)
resolver_addr
).transact(transact)
return self._resolverContract(address=resolver_addr)

@dict_copy
Expand All @@ -304,8 +305,8 @@ def _setup_reverse(self, name, address, transact={}):
else:
name = ''
transact['from'] = address
return self._reverse_registrar().setName(name, transact=transact)
return self._reverse_registrar().functions.setName(name).transact(transact)

def _reverse_registrar(self):
addr = self.ens.owner(normal_name_to_hash(REVERSE_REGISTRAR_DOMAIN))
addr = self.ens.caller.owner(normal_name_to_hash(REVERSE_REGISTRAR_DOMAIN))
return self.web3.eth.contract(address=addr, abi=abis.REVERSE_REGISTRAR)
6 changes: 4 additions & 2 deletions ens/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,13 @@ def init_web3(providers=default):


def customize_web3(w3):
from web3.contract import ConciseContract
from web3.middleware import make_stalecheck_middleware

w3.middleware_onion.remove('name_to_address')
w3.middleware_onion.add(
make_stalecheck_middleware(ACCEPTABLE_STALE_HOURS * 3600),
name='stalecheck',
)
w3.eth.setContractFactory(ConciseContract)
return w3


Expand Down Expand Up @@ -211,3 +209,7 @@ def assert_signer_in_modifier_kwargs(modifier_kwargs):
raise TypeError(ERR_MSG)

return modifier_dict['from']


def none_or_zero_address(addr):
kclowes marked this conversation as resolved.
Show resolved Hide resolved
return not addr or addr == '0x' + '00' * 20
50 changes: 50 additions & 0 deletions tests/core/contracts/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,56 @@ def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT):
return web3.eth.contract(**FALLBACK_FUNCTION_CONTRACT)


CONTRACT_RETURN_ARGS_SOURCE = """
contract CallerTester {
function add(int256 a, int256 b) public payable returns (int256) {
return a + b;
}

function returnMeta() public payable returns (address, bytes memory, uint256, uint, uint) {
return (msg.sender, msg.data, gasleft(), msg.value, block.number);
}
}
"""

CONTRACT_RETURN_ARGS_CODE = "608060405234801561001057600080fd5b506101d4806100206000396000f3fe608060405260043610610045577c01000000000000000000000000000000000000000000000000000000006000350463a5f3c23b811461004a578063c7fa7d661461007f575b600080fd5b61006d6004803603604081101561006057600080fd5b5080359060200135610147565b60408051918252519081900360200190f35b61008761014b565b604051808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001858152602001848152602001838152602001828103825286818151815260200191508051906020019080838360005b838110156101085781810151838201526020016100f0565b50505050905090810190601f1680156101355780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390f35b0190565b600060606000806000336000365a344385955084848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250989e929d50949b509299509097509550505050505056fea165627a7a7230582009a1e09c8cb9406971ab18e5c44bbc09e03adbae9b66c9d6b68a9fa503d495c90029" # noqa: E501

CONTRACT_RETURN_ARGS_RUNTIME = "608060405260043610610045577c01000000000000000000000000000000000000000000000000000000006000350463a5f3c23b811461004a578063c7fa7d661461007f575b600080fd5b61006d6004803603604081101561006057600080fd5b5080359060200135610147565b60408051918252519081900360200190f35b61008761014b565b604051808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001858152602001848152602001838152602001828103825286818151815260200191508051906020019080838360005b838110156101085781810151838201526020016100f0565b50505050905090810190601f1680156101355780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390f35b0190565b600060606000806000336000365a344385955084848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250989e929d50949b509299509097509550505050505056fea165627a7a7230582009a1e09c8cb9406971ab18e5c44bbc09e03adbae9b66c9d6b68a9fa503d495c90029" # noqa: E501

CONTRACT_RETURN_ARGS_ABI = json.loads('[ { "constant": false, "inputs": [ { "name": "a", "type": "int256" }, { "name": "b", "type": "int256" } ], "name": "add", "outputs": [ { "name": "", "type": "int256" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [], "name": "returnMeta", "outputs": [ { "name": "", "type": "address" }, { "name": "", "type": "bytes" }, { "name": "", "type": "uint256" }, { "name": "", "type": "uint256" }, { "name": "", "type": "uint256" } ], "payable": true, "stateMutability": "payable", "type": "function" } ]') # noqa: E501


@pytest.fixture()
def RETURN_ARGS_CODE():
return CONTRACT_RETURN_ARGS_CODE


@pytest.fixture()
def RETURN_ARGS_RUNTIME():
return CONTRACT_RETURN_ARGS_RUNTIME


@pytest.fixture()
def RETURN_ARGS_ABI():
return CONTRACT_RETURN_ARGS_ABI


@pytest.fixture()
def RETURN_ARGS_CONTRACT(RETURN_ARGS_CODE,
RETURN_ARGS_RUNTIME,
RETURN_ARGS_ABI):
return {
'bytecode': RETURN_ARGS_CODE,
'bytecode_runtime': RETURN_ARGS_RUNTIME,
'abi': RETURN_ARGS_ABI,
}


@pytest.fixture()
def ReturnArgsContract(web3, RETURN_ARGS_CONTRACT):
return web3.eth.contract(**RETURN_ARGS_CONTRACT)


class LogFunctions:
LogAnonymous = 0
LogNoArguments = 1
Expand Down
4 changes: 2 additions & 2 deletions tests/core/contracts/test_concise_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ def test_class_construction_sets_class_vars(web3,
assert classic.bytecode_runtime == decode_hex(MATH_RUNTIME)


def test_conciscecontract_keeps_custom_normalizers_on_base(web3):
base_contract = web3.eth.contract()
def test_conciscecontract_keeps_custom_normalizers_on_base(web3, MATH_ABI):
base_contract = web3.eth.contract(abi=MATH_ABI)
# give different normalizers to this base instance
base_contract._return_data_normalizers = base_contract._return_data_normalizers + tuple([None])

Expand Down
124 changes: 124 additions & 0 deletions tests/core/contracts/test_contract_caller_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import pytest

from web3._utils.toolz import (
identity,
)
from web3.exceptions import (
MismatchedABI,
NoABIFunctionsFound,
)


def deploy(web3, Contract, apply_func=identity, args=None):
args = args or []
deploy_txn = Contract.constructor(*args).transact()
deploy_receipt = web3.eth.waitForTransactionReceipt(deploy_txn)
assert deploy_receipt is not None
address = apply_func(deploy_receipt['contractAddress'])
contract = Contract(address=address)
assert contract.address == address
assert len(web3.eth.getCode(contract.address)) > 0
return contract


@pytest.fixture()
def math_contract(web3, MathContract, address_conversion_func):
return deploy(web3, MathContract, address_conversion_func)


@pytest.fixture()
def return_args_contract(web3, ReturnArgsContract, address_conversion_func):
return deploy(web3, ReturnArgsContract, address_conversion_func)


def test_caller_default(math_contract):
result = math_contract.caller.add(3, 5)
assert result == 8


def test_caller_with_parens(math_contract):
result = math_contract.caller().add(3, 5)
assert result == 8


def test_caller_with_no_abi(web3):
contract = web3.eth.contract()
with pytest.raises(NoABIFunctionsFound):
contract.caller.thisFunctionDoesNotExist()


def test_caller_with_a_nonexistent_function(math_contract):
contract = math_contract
with pytest.raises(MismatchedABI):
contract.caller.thisFunctionDoesNotExist()


def test_caller_with_block_identifier(web3, math_contract):
start_num = web3.eth.getBlock('latest').number
assert math_contract.caller.counter() == 0

web3.provider.make_request(method='evm_mine', params=[5])
math_contract.functions.increment().transact()
math_contract.functions.increment().transact()

output1 = math_contract.caller(block_identifier=start_num + 6).counter()
output2 = math_contract.caller(block_identifier=start_num + 7).counter()

assert output1 == 1
assert output2 == 2


def test_caller_with_transaction_keyword(web3, return_args_contract):
address = web3.eth.accounts[1]
transaction_dict = {
'from': address,
'gas': 210000,
'gasPrice': web3.toWei(.001, 'ether'),
'value': 12345,
}
contract = return_args_contract.caller(transaction_dict=transaction_dict)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This dict has been called transaction in enough other places that I think it's more consistent to just use transaction instead of transaction_dict as the keyword argument name.


sender, _, gasLeft, value, _ = contract.returnMeta()

assert address == sender
assert gasLeft <= transaction_dict['gas']
assert value == transaction_dict['value']


def test_caller_with_dict_but_no_transaction_keyword(web3, return_args_contract):
address = web3.eth.accounts[1]
transaction_dict = {
'from': address,
'gas': 210000,
'gasPrice': web3.toWei(.001, 'ether'),
'value': 12345,
}

contract = return_args_contract.caller(transaction_dict)

sender, _, gasLeft, value, _ = contract.returnMeta()

assert address == sender
assert gasLeft <= transaction_dict['gas']
assert value == transaction_dict['value']


def test_caller_with_args_and_no_transaction_keyword(web3, return_args_contract):
address = web3.eth.accounts[1]
transaction_dict = {
'from': address,
'gas': 210000,
'gasPrice': web3.toWei(.001, 'ether'),
'value': 12345,
}

contract = return_args_contract.caller(transaction_dict)

sender, _, gasLeft, value, _ = contract.returnMeta()

assert address == sender
assert gasLeft <= transaction_dict['gas']
assert value == transaction_dict['value']

add_result = contract.add(3, 5)
assert add_result == 8
Loading