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 26 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
67 changes: 61 additions & 6 deletions docs/contracts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ To run this example, you will need to install a few extra features:

from web3 import Web3
from solc import compile_source
from web3.contract import ConciseContract

# Solidity source code
contract_source_code = """
Expand Down Expand Up @@ -89,11 +88,6 @@ To run this example, you will need to install a few extra features:
greeter.functions.greet().call()
))

# When issuing a lot of reads, try this more concise reader:
reader = ConciseContract(greeter)
assert reader.greet() == "Nihao"


Contract Factories
------------------

Expand All @@ -111,6 +105,9 @@ example in :class:`ConciseContract` for specifying an alternate factory.

.. py:class:: ConciseContract(Contract())

.. warning:: Deprecated: This method is deprecated in favor of the :class:`~ContractCaller` API
or the verbose syntax

This variation of :class:`Contract` is designed for more succinct read access,
without making write access more wordy. This comes at a cost of losing
access to features like ``deploy()`` and properties like ``address``. It is
Expand Down Expand Up @@ -145,6 +142,8 @@ example in :class:`ConciseContract` for specifying an alternate factory.

.. py:class:: ImplicitContract(Contract())

.. warning:: Deprecated: This method is deprecated in favor of the verbose syntax

This variation mirrors :py:class:`ConciseContract`, but it invokes all methods as a
transaction rather than a call, so if the classic contract had a method like
``contract.functions.owner.transact()``, you could call it with ``implicit.owner()`` instead.
Expand Down Expand Up @@ -745,3 +744,59 @@ Utils
'_debatingPeriod': 604800,
'_newCurator': True})

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

.. py:class:: ContractCaller

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

There are a number of different ways to invoke the ``ContractCaller``.

For example:

.. testsetup::

import json
from web3 import Web3
w3 = Web3(Web3.EthereumTesterProvider())
bytecode = "0x606060405261022e806100126000396000f360606040523615610074576000357c01000000000000000000000000000000000000000000000000000000009004806316216f391461007657806361bc221a146100995780637cf5dab0146100bc578063a5f3c23b146100e8578063d09de08a1461011d578063dcf537b11461014057610074565b005b610083600480505061016c565b6040518082815260200191505060405180910390f35b6100a6600480505061017f565b6040518082815260200191505060405180910390f35b6100d26004808035906020019091905050610188565b6040518082815260200191505060405180910390f35b61010760048080359060200190919080359060200190919050506101ea565b6040518082815260200191505060405180910390f35b61012a6004805050610201565b6040518082815260200191505060405180910390f35b6101566004808035906020019091905050610217565b6040518082815260200191505060405180910390f35b6000600d9050805080905061017c565b90565b60006000505481565b6000816000600082828250540192505081905550600060005054905080507f3496c3ede4ec3ab3686712aa1c238593ea6a42df83f98a5ec7df9834cfa577c5816040518082815260200191505060405180910390a18090506101e5565b919050565b6000818301905080508090506101fb565b92915050565b600061020d6001610188565b9050610214565b90565b60006007820290508050809050610229565b91905056"
ABI = json.loads('[{"constant":false,"inputs":[],"name":"return13","outputs":[{"name":"result","type":"int256"}],"type":"function"},{"constant":true,"inputs":[],"name":"counter","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"amt","type":"uint256"}],"name":"increment","outputs":[{"name":"result","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"a","type":"int256"},{"name":"b","type":"int256"}],"name":"add","outputs":[{"name":"result","type":"int256"}],"type":"function"},{"constant":false,"inputs":[],"name":"increment","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"a","type":"int256"}],"name":"multiply7","outputs":[{"name":"result","type":"int256"}],"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"event"}]')
contract = w3.eth.contract(abi=ABI, bytecode=bytecode)
deploy_txn = contract.constructor().transact()
deploy_receipt = w3.eth.waitForTransactionReceipt(deploy_txn)
address = deploy_receipt.contractAddress

.. doctest::

>>> myContract = w3.eth.contract(address=address, abi=ABI)
>>> twentyone = myContract.caller.multiply7(3)
>>> twentyone
21

It can also be invoked using parentheses:

.. doctest::

>>> twentyone = myContract.caller().multiply7(3)
>>> twentyone
21

And a transaction dictionary, with or without the ``transaction_dict`` keyword. For example:

.. doctest::

>>> from_address = w3.eth.accounts[1]
>>> twentyone = myContract.caller({'from': from_address}).multiply7(3)
>>> twentyone
21
>>> twentyone = myContract.caller(transaction_dict={'from': from_address}).multiply7(3)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe one more example that includes block_identifier?

>>> twentyone
21

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 @@ -21,6 +21,7 @@
default,
dict_copy,
init_web3,
is_none_or_zero_address,
is_valid_name,
label_to_hash,
normal_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 is_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 is_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 is_none_or_zero_address(address):
address = self.owner(name)
if not address:
if is_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 is_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 is_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 is_none_or_zero_address(owner):
name = '.'.join(pieces)
owner = self.owner(name)
if not owner:
if is_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 is_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 is_none_or_zero_address(addr):
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
10 changes: 8 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 Expand Up @@ -133,3 +133,9 @@ def getValue():

with pytest.raises(AttributeError, match=r'Namespace collision .* with ConciseContract API.'):
concise_contract.getValue()


def test_concisecontract_deprecation_warning(web3, StringContract):
contract = deploy(web3, StringContract, args=["blarg"])
with pytest.warns(DeprecationWarning):
ConciseContract(contract)
Loading