diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 31df3fd496..43a7176bdc 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 5.0.0-alpha.4 +current_version = 5.0.0-alpha.5 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[^.]*)\.(?P\d+))? diff --git a/.circleci/config.yml b/.circleci/config.yml index c32b23cc5f..0d67ad645c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -193,8 +193,8 @@ jobs: - image: circleci/python:3.6-stretch environment: TOXENV: py36-integration-parity-ipc - PARITY_VERSION: v1.11.11 - PARITY_OS: debian + PARITY_VERSION: v2.3.5 + PARITY_OS: linux py36-integration-parity-http: <<: *parity_steps @@ -202,8 +202,8 @@ jobs: - image: circleci/python:3.6-stretch environment: TOXENV: py36-integration-parity-http - PARITY_VERSION: v1.11.11 - PARITY_OS: debian + PARITY_VERSION: v2.3.5 + PARITY_OS: linux py36-integration-parity-ws: <<: *parity_steps @@ -211,8 +211,8 @@ jobs: - image: circleci/python:3.6-stretch environment: TOXENV: py36-integration-parity-ws - PARITY_VERSION: v1.11.11 - PARITY_OS: debian + PARITY_VERSION: v2.3.5 + PARITY_OS: linux py36-integration-ethtester-pyevm: <<: *common @@ -292,8 +292,8 @@ jobs: - image: circleci/python:3.7-stretch environment: TOXENV: py37-integration-parity-ipc - PARITY_VERSION: v1.11.11 - PARITY_OS: debian + PARITY_VERSION: v2.3.5 + PARITY_OS: linux py37-integration-parity-http: <<: *parity_steps @@ -301,8 +301,8 @@ jobs: - image: circleci/python:3.7-stretch environment: TOXENV: py37-integration-parity-http - PARITY_VERSION: v1.11.11 - PARITY_OS: debian + PARITY_VERSION: v2.3.5 + PARITY_OS: linux py37-integration-parity-ws: <<: *parity_steps @@ -310,8 +310,8 @@ jobs: - image: circleci/python:3.7-stretch environment: TOXENV: py37-integration-parity-ws - PARITY_VERSION: v1.11.11 - PARITY_OS: debian + PARITY_VERSION: v2.3.5 + PARITY_OS: linux py37-integration-ethtester-pyevm: <<: *common diff --git a/docs/contracts.rst b/docs/contracts.rst index d0aa0e7555..ae7f57bd29 100644 --- a/docs/contracts.rst +++ b/docs/contracts.rst @@ -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 = """ @@ -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 ------------------ @@ -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 @@ -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. @@ -745,3 +744,63 @@ 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`` keyword. +You can also optionally include a block identifier. For example: + +.. doctest:: + + >>> from_address = w3.eth.accounts[1] + >>> twentyone = myContract.caller({'from': from_address}).multiply7(3) + >>> twentyone + 21 + >>> twentyone = myContract.caller(transaction={'from': from_address}).multiply7(3) + >>> twentyone + 21 + >>> twentyone = myContract.caller(block_identifier='latest').multiply7(3) + >>> 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. diff --git a/docs/filters.rst b/docs/filters.rst index fd169dbccc..5f4355f6df 100644 --- a/docs/filters.rst +++ b/docs/filters.rst @@ -4,6 +4,15 @@ Filtering .. py:module:: web3.utils.filters +.. note :: + + Most one-liners below assume ``w3`` to be a :class:`web3.Web3` instance; + obtainable, for example, with: + + .. code-block:: python + + from web3.auto import w3 + The :meth:`web3.eth.Eth.filter` method can be used to setup filters for: * Pending Transactions: ``web3.eth.filter('pending')`` @@ -22,14 +31,13 @@ The :meth:`web3.eth.Eth.filter` method can be used to setup filters for: .. code-block:: python - event_filter = web3.eth.filter({"address": contract_address}) + event_filter = w3.eth.filter({"address": contract_address}) * Attaching to an existing filter .. code-block:: python - from web3.auto import w3 - existing_filter = web3.eth.filter(filter_id="0x0") + existing_filter = w3.eth.filter(filter_id="0x0") .. note :: @@ -86,27 +94,27 @@ Block and Transaction Filter Classes .. py:class:: BlockFilter(...) -BlockFilter is a subclass of :class:``Filter``. +``BlockFilter`` is a subclass of :class:`Filter`. You can setup a filter for new blocks using ``web3.eth.filter('latest')`` which -will return a new :py:class:`BlockFilter` object. +will return a new :class:`BlockFilter` object. .. code-block:: python - >>> new_block_filter = web.eth.filter('latest') - >>> new_block_filter.get_new_entries() + new_block_filter = w3.eth.filter('latest') + new_block_filter.get_new_entries() .. py:class:: TransactionFilter(...) -TransactionFilter is a subclass of :class:``Filter``. +``TransactionFilter`` is a subclass of :class:`Filter`. You can setup a filter for new blocks using ``web3.eth.filter('pending')`` which -will return a new :py:class:`BlockFilter` object. +will return a new :class:`BlockFilter` object. .. code-block:: python - >>> new_transaction_filter = web.eth.filter('pending') - >>> new_transaction_filter.get_new_entries() + new_transaction_filter = w3.eth.filter('pending') + new_transaction_filter.get_new_entries() Event Log Filters diff --git a/docs/overview.rst b/docs/overview.rst index 33dbb51155..4235fb4d2f 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -163,6 +163,18 @@ Type Conversions >>> Web3.toInt(hexstr='000F') 15 +.. py:method:: Web3.toJSON(obj) + + Takes a variety of inputs and returns its JSON equivalent. + + + .. code-block:: python + + >>> Web3.toJSON(3) + '3' + >>> Web3.toJSON({'one': 1}) + '{"one": 1}' + .. _overview_currency_conversions: Currency Conversions diff --git a/docs/releases.rst b/docs/releases.rst index cc03c35658..64a428992b 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -2,6 +2,40 @@ Release Notes ============= +v5.0.0-alpha.5 +-------------- + +Released February 13th, 2019 + +- Breaking Changes + + - Remove deprecated ``buildTransaction``, ``call``, ``deploy``, + ``estimateGas``, and ``transact`` methods + - `#1232 `_ + +- Features + + - Adds ``Web3.toJSON`` method + - `#1173 `_ + - Contract Caller API Implemented + - `#1227 `_ + - Add Geth POA middleware to use Rinkeby with Infura Auto + - `#1234 `_ + - Add manifest and input argument validation to ``pm.release_package()`` + - `#1237 `_ + +- Misc + + - Clean up intro and block/tx sections in Filter docs + - `#1223 `_ + - Remove unnecessary ``EncodingError`` exception catching + - `#1224 `_ + - Improvements to ``merge_args_and_kwargs`` utility function + - `#1228 `_ + - Update vyper registry assets + - `#1242 `_ + + v5.0.0-alpha.4 -------------- diff --git a/ens/main.py b/ens/main.py index e0e1d4b229..9789a2d405 100644 --- a/ens/main.py +++ b/ens/main.py @@ -21,6 +21,7 @@ default, dict_copy, init_web3, + is_none_or_zero_address, is_valid_name, label_to_hash, normal_name_to_hash, @@ -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={}): @@ -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 @@ -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={}): @@ -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) @@ -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) @@ -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={}): @@ -265,10 +268,10 @@ 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) @@ -276,25 +279,23 @@ def _first_owner(self, name): 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 @@ -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) diff --git a/ens/utils.py b/ens/utils.py index 417a94b51a..f420549cdb 100644 --- a/ens/utils.py +++ b/ens/utils.py @@ -57,7 +57,6 @@ 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') @@ -65,7 +64,6 @@ def customize_web3(w3): make_stalecheck_middleware(ACCEPTABLE_STALE_HOURS * 3600), name='stalecheck', ) - w3.eth.setContractFactory(ConciseContract) return w3 @@ -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 diff --git a/setup.py b/setup.py index 72fe386759..7b6381cc70 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ setup( name='web3', # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility. - version='5.0.0-alpha.4', + version='5.0.0-alpha.5', description="""Web3.py""", long_description_markdown_filename='README.md', author='Piper Merriam', @@ -68,11 +68,12 @@ url='https://github.com/ethereum/web3.py', include_package_data=True, install_requires=[ - "eth-abi>=2.0.0b5,<3.0.0", + "eth-abi>=2.0.0b6,<3.0.0", "eth-account>=0.2.1,<0.4.0", "eth-hash[pycryptodome]>=0.2.0,<1.0.0", + "eth-typing>=2.0.0,<3.0.0", "eth-utils>=1.3.0,<2.0.0", - "ethpm>=0.1.4a10,<1.0.0", + "ethpm>=0.1.4a12,<1.0.0", "hexbytes>=0.1.0,<1.0.0", "lru-dict>=1.1.6,<2.0.0", "requests>=2.16.0,<3.0.0", diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index ca14b2da32..7dc0c7587b 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -527,6 +527,69 @@ def FallballFunctionContract(web3, FALLBACK_FUNCTION_CONTRACT): return web3.eth.contract(**FALLBACK_FUNCTION_CONTRACT) +CONTRACT_CALLER_TESTER_SOURCE = """ +contract CallerTester { + int public count; + + function add(int256 a, int256 b) public payable returns (int256) { + return a + b; + } + + function increment() public returns (int256) { + return count += 1; + } + + function counter() public payable returns (int256) { + return count; + } + + function returnMeta() public payable returns (address, bytes memory, uint256, uint, uint) { + return (msg.sender, msg.data, gasleft(), msg.value, block.number); + } +} +""" + + +CONTRACT_CALLER_TESTER_CODE = "608060405234801561001057600080fd5b50610241806100206000396000f3fe608060405260043610610066577c0100000000000000000000000000000000000000000000000000000000600035046306661abd811461006b57806361bc221a14610092578063a5f3c23b1461009a578063c7fa7d66146100bd578063d09de08a14610185575b600080fd5b34801561007757600080fd5b5061008061019a565b60408051918252519081900360200190f35b6100806101a0565b610080600480360360408110156100b057600080fd5b50803590602001356101a6565b6100c56101aa565b604051808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001858152602001848152602001838152602001828103825286818151815260200191508051906020019080838360005b8381101561014657818101518382015260200161012e565b50505050905090810190601f1680156101735780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390f35b34801561019157600080fd5b50610080610207565b60005481565b60005490565b0190565b600060606000806000336000365a344385955084848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250989e929d50949b5092995090975095505050505050565b60008054600101908190559056fea165627a7a72305820ffe1620e420efa326b9c5e4ef9f93cac71cf986196246c7966d71a39259899b10029" # noqa: E501 + + +CONTRACT_CALLER_TESTER_RUNTIME = "608060405260043610610066577c0100000000000000000000000000000000000000000000000000000000600035046306661abd811461006b57806361bc221a14610092578063a5f3c23b1461009a578063c7fa7d66146100bd578063d09de08a14610185575b600080fd5b34801561007757600080fd5b5061008061019a565b60408051918252519081900360200190f35b6100806101a0565b610080600480360360408110156100b057600080fd5b50803590602001356101a6565b6100c56101aa565b604051808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001858152602001848152602001838152602001828103825286818151815260200191508051906020019080838360005b8381101561014657818101518382015260200161012e565b50505050905090810190601f1680156101735780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390f35b34801561019157600080fd5b50610080610207565b60005481565b60005490565b0190565b600060606000806000336000365a344385955084848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250989e929d50949b5092995090975095505050505050565b60008054600101908190559056fea165627a7a72305820ffe1620e420efa326b9c5e4ef9f93cac71cf986196246c7966d71a39259899b10029" # noqa: E501 + + +CONTRACT_CALLER_TESTER_ABI = json.loads('[ { "constant": true, "inputs": [], "name": "count", "outputs": [ { "name": "", "type": "int256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "counter", "outputs": [ { "name": "", "type": "int256" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "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" }, { "constant": false, "inputs": [], "name": "increment", "outputs": [ { "name": "", "type": "int256" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" } ]') # noqa: E501 + + +@pytest.fixture() +def CALLER_TESTER_CODE(): + return CONTRACT_CALLER_TESTER_CODE + + +@pytest.fixture() +def CALLER_TESTER_RUNTIME(): + return CONTRACT_CALLER_TESTER_RUNTIME + + +@pytest.fixture() +def CALLER_TESTER_ABI(): + return CONTRACT_CALLER_TESTER_ABI + + +@pytest.fixture() +def CALLER_TESTER_CONTRACT(CALLER_TESTER_CODE, + CALLER_TESTER_RUNTIME, + CALLER_TESTER_ABI): + return { + 'bytecode': CALLER_TESTER_CODE, + 'bytecode_runtime': CALLER_TESTER_RUNTIME, + 'abi': CALLER_TESTER_ABI, + } + + +@pytest.fixture() +def CallerTesterContract(web3, CALLER_TESTER_CONTRACT): + return web3.eth.contract(**CALLER_TESTER_CONTRACT) + + class LogFunctions: LogAnonymous = 0 LogNoArguments = 1 @@ -581,8 +644,7 @@ def some_address(address_conversion_func): return address_conversion_func('0x5B2063246F2191f18F2675ceDB8b28102e957458') -def invoke_contract(api_style=None, - api_call_desig='call', +def invoke_contract(api_call_desig='call', contract=None, contract_function=None, func_args=[], @@ -592,34 +654,27 @@ def invoke_contract(api_style=None, if api_call_desig not in allowable_call_desig: raise ValueError("allowable_invoke_method must be one of: %s" % allowable_call_desig) - if api_style == 'func_first': - function = contract.functions[contract_function] - result = getattr(function(*func_args, **func_kwargs), api_call_desig)(tx_params) - elif api_style == 'func_last': - api_call_cls = getattr(contract, api_call_desig) - with pytest.deprecated_call(): - result = getattr(api_call_cls(tx_params), contract_function)(*func_args, **func_kwargs) - else: - raise ValueError("api_style must be 'func_first or func_last'") + function = contract.functions[contract_function] + result = getattr(function(*func_args, **func_kwargs), api_call_desig)(tx_params) return result -@pytest.fixture(params=['func_first', 'func_last']) +@pytest.fixture def transact(request): - return functools.partial(invoke_contract, request.param, api_call_desig='transact') + return functools.partial(invoke_contract, api_call_desig='transact') -@pytest.fixture(params=['func_first', 'func_last']) +@pytest.fixture def call(request): - return functools.partial(invoke_contract, request.param, api_call_desig='call') + return functools.partial(invoke_contract, api_call_desig='call') -@pytest.fixture(params=['func_first', 'func_last']) +@pytest.fixture def estimateGas(request): - return functools.partial(invoke_contract, request.param, api_call_desig='estimateGas') + return functools.partial(invoke_contract, api_call_desig='estimateGas') -@pytest.fixture(params=['func_first', 'func_last']) +@pytest.fixture def buildTransaction(request): - return functools.partial(invoke_contract, request.param, api_call_desig='buildTransaction') + return functools.partial(invoke_contract, api_call_desig='buildTransaction') diff --git a/tests/core/contracts/test_concise_contract.py b/tests/core/contracts/test_concise_contract.py index 982d36d731..fa2f0d58a7 100644 --- a/tests/core/contracts/test_concise_contract.py +++ b/tests/core/contracts/test_concise_contract.py @@ -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]) @@ -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) diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index b65b59ad66..203324275d 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -24,6 +24,7 @@ BlockNumberOutofRange, InvalidAddress, MismatchedABI, + NoABIFound, NoABIFunctionsFound, ValidationError, ) @@ -527,7 +528,7 @@ def test_function_multiple_possible_encodings(web3): def test_function_no_abi(web3): contract = web3.eth.contract() - with pytest.raises(NoABIFunctionsFound): + with pytest.raises(NoABIFound): contract.functions.thisFunctionDoesNotExist().call() diff --git a/tests/core/contracts/test_contract_caller_interface.py b/tests/core/contracts/test_contract_caller_interface.py new file mode 100644 index 0000000000..1a66226a7c --- /dev/null +++ b/tests/core/contracts/test_contract_caller_interface.py @@ -0,0 +1,170 @@ +import pytest + +from web3._utils.toolz import ( + identity, +) +from web3.exceptions import ( + MismatchedABI, + NoABIFound, + 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 address(web3): + return web3.eth.accounts[1] + + +@pytest.fixture() +def math_contract(web3, MathContract, address_conversion_func): + return deploy(web3, MathContract, address_conversion_func) + + +@pytest.fixture() +def caller_tester_contract(web3, CallerTesterContract, address_conversion_func): + return deploy(web3, CallerTesterContract, address_conversion_func) + + +@pytest.fixture() +def transaction_dict(web3, address): + return { + 'from': address, + 'gas': 210000, + 'gasPrice': web3.toWei(.001, 'ether'), + 'value': 12345, + } + + +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(NoABIFound): + contract.caller.thisFunctionDoesNotExist() + + +def test_caller_with_no_abi_and_parens(web3): + contract = web3.eth.contract() + with pytest.raises(NoABIFound): + contract.caller().thisFunctionDoesNotExist() + + +def test_caller_with_empty_abi_and_parens(web3): + contract = web3.eth.contract(abi=[]) + with pytest.raises(NoABIFunctionsFound): + contract.caller().thisFunctionDoesNotExist() + + +def test_caller_with_empty_abi(web3): + contract = web3.eth.contract(abi=[]) + 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_block_identifier_and_transaction_dict(web3, + caller_tester_contract, + transaction_dict, + address): + start_num = web3.eth.getBlock('latest').number + assert caller_tester_contract.caller.counter() == 0 + + web3.provider.make_request(method='evm_mine', params=[5]) + caller_tester_contract.functions.increment().transact() + + block_id = start_num + 6 + contract = caller_tester_contract.caller( + transaction=transaction_dict, + block_identifier=block_id + ) + + sender, _, gasLeft, value, block_num = contract.returnMeta() + counter = contract.counter() + + assert sender == address + assert gasLeft <= transaction_dict['gas'] + assert value == transaction_dict['value'] + assert block_num == block_id + assert counter == 1 + + +def test_caller_with_transaction_keyword(web3, + caller_tester_contract, + transaction_dict, + address): + contract = caller_tester_contract.caller(transaction=transaction_dict) + + 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, + caller_tester_contract, + transaction_dict, + address): + contract = caller_tester_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, + caller_tester_contract, + transaction_dict, + address): + contract = caller_tester_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 diff --git a/tests/core/contracts/test_implicit_contract.py b/tests/core/contracts/test_implicit_contract.py index 5defe47617..b29af3c4ae 100644 --- a/tests/core/contracts/test_implicit_contract.py +++ b/tests/core/contracts/test_implicit_contract.py @@ -99,3 +99,8 @@ def test_implicitcontract_transact_override(math_contract, get_transaction_count assert get_transaction_count(blocknum) == starting_txns # Check that no blocks were mined assert get_transaction_count("pending") == (blocknum, 0) + + +def test_implicitcontract_deprecation_warning(math_contract): + with pytest.warns(DeprecationWarning): + math_contract.counter(transact={}) diff --git a/tests/core/pm-module/conftest.py b/tests/core/pm-module/conftest.py index 0ac2ed46e4..49ca700225 100644 --- a/tests/core/pm-module/conftest.py +++ b/tests/core/pm-module/conftest.py @@ -165,7 +165,7 @@ def sol_registry(w3): def vy_registry(w3): registry_path = ASSETS_DIR / "vyper_registry" - manifest = json.loads((registry_path / "1.0.0.json").read_text().rstrip('\n')) + manifest = json.loads((registry_path / "0.1.0.json").read_text().rstrip('\n')) registry_package = Package(manifest, w3) registry_deployer = Deployer(registry_package) deployed_registry_package = registry_deployer.deploy("registry") diff --git a/tests/core/pm-module/test_ens_integration.py b/tests/core/pm-module/test_ens_integration.py index 239171f413..7681489bbf 100644 --- a/tests/core/pm-module/test_ens_integration.py +++ b/tests/core/pm-module/test_ens_integration.py @@ -2,7 +2,6 @@ from eth_utils import ( to_bytes, - to_canonical_address, ) from ethpm import ( ASSETS_DIR, @@ -136,9 +135,9 @@ def test_web3_ens(ens): w3.ens.setup_address('tester.eth', registry.address) actual_addr = ens.address('tester.eth') w3.pm.set_registry('tester.eth') - assert w3.pm.registry.address == to_canonical_address(actual_addr) - w3.pm.release_package('pkg', 'v1', 'website.com') - pkg_name, version, manifest_uri = w3.pm.get_release_data('pkg', 'v1') - assert pkg_name == 'pkg' - assert version == 'v1' - assert manifest_uri == 'website.com' + assert w3.pm.registry.address == actual_addr + w3.pm.release_package('owned', '1.0.0', 'ipfs://QmbeVyFLSuEUxiXKwSsEjef6icpdTdA4kGG9BcrJXKNKUW') + pkg_name, version, manifest_uri = w3.pm.get_release_data('owned', '1.0.0') + assert pkg_name == 'owned' + assert version == '1.0.0' + assert manifest_uri == 'ipfs://QmbeVyFLSuEUxiXKwSsEjef6icpdTdA4kGG9BcrJXKNKUW' diff --git a/tests/core/pm-module/test_registry_integration.py b/tests/core/pm-module/test_registry_integration.py index 20d9ff8903..4993ab0d12 100644 --- a/tests/core/pm-module/test_registry_integration.py +++ b/tests/core/pm-module/test_registry_integration.py @@ -62,7 +62,7 @@ def test_pm_set_solidity_registry(empty_sol_registry, fresh_w3): def test_pm_must_set_registry_before_all_registry_interaction_functions(fresh_w3): with pytest.raises(PMError): fresh_w3.pm.release_package( - "package", "1.0.0", "ipfs://Qme4otpS88NV8yQi8TfTP89EsQC5bko3F5N1yhRoi6cwGe" + "package", "1.0.0", "ipfs://QmbeVyFLSuEUxiXKwSsEjef6icpdTdA4kGG9BcrJXKNKUW" ) with pytest.raises(PMError): fresh_w3.pm.get_release_id_data(b"invalid_release_id") @@ -88,21 +88,21 @@ def test_pm_must_set_registry_before_all_registry_interaction_functions(fresh_w3 def test_pm_release_package(registry_getter, w3): w3.pm.registry = registry_getter w3.pm.release_package( - "package123", "1.0.0", "ipfs://Qme4otpS88NV8yQi8TfTP89EsQC5bko3F5N1yhRoi6cwGE" + "escrow", "1.0.0", "ipfs://QmPDwMHk8e1aMEZg3iKsUiPSkhHkywpGB3KHKM52RtGrkv" ) w3.pm.release_package( - "package456", "1.0.0", "ipfs://Qme4otpS88NV8yQi8TfTP89EsQC5bko3F5N1yhRoi6cwGI" + "owned", "1.0.0", "ipfs://QmbeVyFLSuEUxiXKwSsEjef6icpdTdA4kGG9BcrJXKNKUW" ) - release_id_1 = w3.pm.get_release_id("package123", "1.0.0") - release_id_2 = w3.pm.get_release_id("package456", "1.0.0") + release_id_1 = w3.pm.get_release_id("escrow", "1.0.0") + release_id_2 = w3.pm.get_release_id("owned", "1.0.0") package_data_1 = w3.pm.get_release_id_data(release_id_1) package_data_2 = w3.pm.get_release_id_data(release_id_2) - assert package_data_1[0] == "package123" + assert package_data_1[0] == "escrow" assert package_data_1[1] == "1.0.0" - assert package_data_1[2] == "ipfs://Qme4otpS88NV8yQi8TfTP89EsQC5bko3F5N1yhRoi6cwGE" - assert package_data_2[0] == "package456" + assert package_data_1[2] == "ipfs://QmPDwMHk8e1aMEZg3iKsUiPSkhHkywpGB3KHKM52RtGrkv" + assert package_data_2[0] == "owned" assert package_data_2[1] == "1.0.0" - assert package_data_2[2] == "ipfs://Qme4otpS88NV8yQi8TfTP89EsQC5bko3F5N1yhRoi6cwGI" + assert package_data_2[2] == "ipfs://QmbeVyFLSuEUxiXKwSsEjef6icpdTdA4kGG9BcrJXKNKUW" @pytest.mark.parametrize( diff --git a/tests/core/providers/test_auto_provider.py b/tests/core/providers/test_auto_provider.py index 27e83a1a84..eae0e5d020 100644 --- a/tests/core/providers/test_auto_provider.py +++ b/tests/core/providers/test_auto_provider.py @@ -33,7 +33,7 @@ def test_load_provider_from_env(monkeypatch, uri, expected_type, expected_attrs) assert getattr(provider, attr) == val -@pytest.mark.parametrize('environ_name', ['INFURA_API_KEY', 'WEB3_INFURA_API_KEY']) +@pytest.mark.parametrize('environ_name', ['WEB3_INFURA_API_KEY', 'WEB3_INFURA_PROJECT_ID']) def test_web3_auto_infura_empty_key(monkeypatch, caplog, environ_name): monkeypatch.setenv('WEB3_INFURA_SCHEME', 'https') monkeypatch.setenv(environ_name, '') @@ -41,7 +41,7 @@ def test_web3_auto_infura_empty_key(monkeypatch, caplog, environ_name): importlib.reload(infura) assert len(caplog.record_tuples) == 1 logger, level, msg = caplog.record_tuples[0] - assert 'WEB3_INFURA_API_KEY' in msg + assert 'WEB3_INFURA_PROJECT_ID' in msg assert level == logging.WARNING w3 = infura.w3 @@ -49,7 +49,7 @@ def test_web3_auto_infura_empty_key(monkeypatch, caplog, environ_name): assert getattr(w3.provider, 'endpoint_uri') == 'https://mainnet.infura.io/' -@pytest.mark.parametrize('environ_name', ['INFURA_API_KEY', 'WEB3_INFURA_API_KEY']) +@pytest.mark.parametrize('environ_name', ['WEB3_INFURA_API_KEY', 'WEB3_INFURA_PROJECT_ID']) def test_web3_auto_infura_deleted_key(monkeypatch, caplog, environ_name): monkeypatch.setenv('WEB3_INFURA_SCHEME', 'https') monkeypatch.delenv(environ_name, raising=False) @@ -57,7 +57,7 @@ def test_web3_auto_infura_deleted_key(monkeypatch, caplog, environ_name): importlib.reload(infura) assert len(caplog.record_tuples) == 1 logger, level, msg = caplog.record_tuples[0] - assert 'WEB3_INFURA_API_KEY' in msg + assert 'WEB3_INFURA_PROJECT_ID' in msg assert level == logging.WARNING w3 = infura.w3 @@ -65,12 +65,42 @@ def test_web3_auto_infura_deleted_key(monkeypatch, caplog, environ_name): assert getattr(w3.provider, 'endpoint_uri') == 'https://mainnet.infura.io/' -@pytest.mark.parametrize('environ_name', ['INFURA_API_KEY', 'WEB3_INFURA_API_KEY']) +@pytest.mark.parametrize('environ_name', ['WEB3_INFURA_API_KEY', 'WEB3_INFURA_PROJECT_ID']) +def test_web3_auto_infura_websocket_empty_key(monkeypatch, caplog, environ_name): + monkeypatch.setenv(environ_name, '') + + importlib.reload(infura) + assert len(caplog.record_tuples) == 1 + logger, level, msg = caplog.record_tuples[0] + assert 'WEB3_INFURA_PROJECT_ID' in msg + assert level == logging.WARNING + + w3 = infura.w3 + assert isinstance(w3.provider, WebsocketProvider) + assert getattr(w3.provider, 'endpoint_uri') == 'wss://mainnet.infura.io/ws/' + + +@pytest.mark.parametrize('environ_name', ['WEB3_INFURA_API_KEY', 'WEB3_INFURA_PROJECT_ID']) +def test_web3_auto_infura_websocket_deleted_key(monkeypatch, caplog, environ_name): + monkeypatch.delenv(environ_name, raising=False) + + importlib.reload(infura) + assert len(caplog.record_tuples) == 1 + logger, level, msg = caplog.record_tuples[0] + assert 'WEB3_INFURA_PROJECT_ID' in msg + assert level == logging.WARNING + + w3 = infura.w3 + assert isinstance(w3.provider, WebsocketProvider) + assert getattr(w3.provider, 'endpoint_uri') == 'wss://mainnet.infura.io/ws/' + + +@pytest.mark.parametrize('environ_name', ['WEB3_INFURA_API_KEY', 'WEB3_INFURA_PROJECT_ID']) def test_web3_auto_infura(monkeypatch, caplog, environ_name): monkeypatch.setenv('WEB3_INFURA_SCHEME', 'https') API_KEY = 'aoeuhtns' monkeypatch.setenv(environ_name, API_KEY) - expected_url = 'https://%s/%s' % (infura.INFURA_MAINNET_DOMAIN, API_KEY) + expected_url = 'https://%s/v3/%s' % (infura.INFURA_MAINNET_DOMAIN, API_KEY) importlib.reload(infura) assert len(caplog.record_tuples) == 0 @@ -80,10 +110,16 @@ def test_web3_auto_infura(monkeypatch, caplog, environ_name): assert getattr(w3.provider, 'endpoint_uri') == expected_url -def test_web3_auto_infura_websocket_default(caplog): +@pytest.mark.parametrize('environ_name', ['WEB3_INFURA_API_KEY', 'WEB3_INFURA_PROJECT_ID']) +def test_web3_auto_infura_websocket_default(monkeypatch, caplog, environ_name): + monkeypatch.setenv('WEB3_INFURA_SCHEME', 'wss') + API_KEY = 'aoeuhtns' + monkeypatch.setenv(environ_name, API_KEY) + expected_url = 'wss://%s/ws/v3/%s' % (infura.INFURA_MAINNET_DOMAIN, API_KEY) + importlib.reload(infura) assert len(caplog.record_tuples) == 0 w3 = infura.w3 assert isinstance(w3.provider, WebsocketProvider) - assert getattr(w3.provider, 'endpoint_uri') == 'wss://mainnet.infura.io/ws' + assert getattr(w3.provider, 'endpoint_uri') == expected_url diff --git a/tests/core/web3-module/test_conversions.py b/tests/core/web3-module/test_conversions.py index 275b512952..6c347df642 100644 --- a/tests/core/web3-module/test_conversions.py +++ b/tests/core/web3-module/test_conversions.py @@ -2,7 +2,14 @@ import pytest +from hexbytes import ( + HexBytes, +) + from web3 import Web3 +from web3.datastructures import ( + AttributeDict, +) @pytest.mark.parametrize( @@ -192,3 +199,52 @@ def test_to_hex_text(val, expected): ) def test_to_hex_cleanup_only(val, expected): assert Web3.toHex(hexstr=val) == expected + + +@pytest.mark.parametrize( + 'val, expected', + ( + (AttributeDict({'one': HexBytes('0x1')}), '{"one": "0x01"}'), + (AttributeDict({'two': HexBytes(2)}), '{"two": "0x02"}'), + (AttributeDict({ + 'three': AttributeDict({ + 'four': 4 + }) + }), '{"three": {"four": 4}}'), + ({'three': 3}, '{"three": 3}'), + ), +) +def test_to_json(val, expected): + assert Web3.toJSON(val) == expected + + +@pytest.mark.parametrize( + 'tx, expected', + ( + ( + AttributeDict({ + 'blockHash': HexBytes( + '0x849044202a39ae36888481f90d62c3826bca8269c2716d7a38696b4f45e61d83' + ), + 'blockNumber': 6928809, + 'from': '0xDEA141eF43A2fdF4e795adA55958DAf8ef5FA619', + 'gas': 21000, + 'gasPrice': 19110000000, + 'hash': HexBytes( + '0x1ccddd19830e998d7cf4d921b19fafd5021c9d4c4ba29680b66fb535624940fc' + ), + 'input': '0x', + 'nonce': 5522, + 'r': HexBytes('0x71ef3eed6242230a219d9dc7737cb5a3a16059708ee322e96b8c5774105b9b00'), + 's': HexBytes('0x48a076afe10b4e1ae82ef82b747e9be64e0bbb1cc90e173db8d53e7baba8ac46'), + 'to': '0x3a84E09D30476305Eda6b2DA2a4e199E2Dd1bf79', + 'transactionIndex': 8, + 'v': 27, + 'value': 2907000000000000 + }), + '{"blockHash": "0x849044202a39ae36888481f90d62c3826bca8269c2716d7a38696b4f45e61d83", "blockNumber": 6928809, "from": "0xDEA141eF43A2fdF4e795adA55958DAf8ef5FA619", "gas": 21000, "gasPrice": 19110000000, "hash": "0x1ccddd19830e998d7cf4d921b19fafd5021c9d4c4ba29680b66fb535624940fc", "input": "0x", "nonce": 5522, "r": "0x71ef3eed6242230a219d9dc7737cb5a3a16059708ee322e96b8c5774105b9b00", "s": "0x48a076afe10b4e1ae82ef82b747e9be64e0bbb1cc90e173db8d53e7baba8ac46", "to": "0x3a84E09D30476305Eda6b2DA2a4e199E2Dd1bf79", "transactionIndex": 8, "v": 27, "value": 2907000000000000}' # noqa: E501 + ), + ), +) +def test_to_json_with_transaction(tx, expected): + assert Web3.toJSON(tx) == expected diff --git a/tests/ens/test_get_registry.py b/tests/ens/test_get_registry.py index 6bc85c2329..645fcff90d 100644 --- a/tests/ens/test_get_registry.py +++ b/tests/ens/test_get_registry.py @@ -9,7 +9,7 @@ def test_resolver_empty(ens): - with patch.object(ens.ens, 'resolver', return_value=None): + with patch.object(ens.ens.caller, 'resolver', return_value=None): assert ens.resolver('') is None diff --git a/tests/ens/test_setup_address.py b/tests/ens/test_setup_address.py index 41e63b2cb0..236c25cbf5 100644 --- a/tests/ens/test_setup_address.py +++ b/tests/ens/test_setup_address.py @@ -71,7 +71,7 @@ def test_set_address(ens, name, full_name, namehash_hex, TEST_ADDRESS): assert is_same_address(ens.address(name), TEST_ADDRESS) # check that the correct namehash is set: - assert is_same_address(ens.resolver(normal_name).addr(namehash), TEST_ADDRESS) + assert is_same_address(ens.resolver(normal_name).caller.addr(namehash), TEST_ADDRESS) # check that the correct owner is set: assert ens.owner(name) == owner diff --git a/tests/ens/test_setup_name.py b/tests/ens/test_setup_name.py index 9ceef3459a..2d8fbc76fa 100644 --- a/tests/ens/test_setup_name.py +++ b/tests/ens/test_setup_name.py @@ -64,7 +64,7 @@ def test_setup_name(ens, name, normalized_name, namehash_hex): # check that the correct namehash is set: node = Web3.toBytes(hexstr=namehash_hex) - assert ens.resolver(normalized_name).addr(node) == address + assert ens.resolver(normalized_name).caller.addr(node) == address # check that the correct owner is set: assert ens.owner(name) == owner diff --git a/tests/integration/parity/install_parity.py b/tests/integration/parity/install_parity.py index 9eda35e01e..c2ab2e7c48 100644 --- a/tests/integration/parity/install_parity.py +++ b/tests/integration/parity/install_parity.py @@ -21,9 +21,10 @@ "v1.9.1": "1_9_1", "v1.10.4": "1_10_4", "v1.11.11": "1_11_11", + "v2.3.5": "2_3_5", } ARCHITECTURE = 'x86_64' -OS = os.getenv('PARITY_OS', 'debian') +OS = os.getenv('PARITY_OS', 'linux') @toolz.curry @@ -63,7 +64,7 @@ def get_executable_path(version_string): def install_parity(version_string): if version_string not in VERSION_STRINGS.keys(): - raise ValueError("{0} is not an accepted version identifier.") + raise ValueError("{0} is not an accepted version identifier.".format(version_string)) path = get_executable_path(version_string) diff --git a/web3/__init__.py b/web3/__init__.py index 90c63b5692..2dafeba539 100644 --- a/web3/__init__.py +++ b/web3/__init__.py @@ -11,7 +11,7 @@ if sys.version_info < (3, 5): raise EnvironmentError( "Python 3.5 or above is required. " - "Note that support for Python 3.5 will be remove in web3.py v5") + "Note that support for Python 3.5 will be removed in web3.py v5") from eth_account import Account # noqa: E402 from web3.main import Web3 # noqa: E402 diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py index 9ccefeac8c..cca0d4edf0 100644 --- a/web3/_utils/abi.py +++ b/web3/_utils/abi.py @@ -4,6 +4,11 @@ ) import itertools import re +from typing import ( + Any, + Optional, + Union, +) from eth_abi import ( decoding, @@ -12,10 +17,17 @@ from eth_abi.codec import ( ABICodec, ) +from eth_abi.grammar import ( + ABIType, + parse, +) from eth_abi.registry import ( BaseEquals, registry as default_registry, ) +from eth_typing import ( + TypeStr, +) from eth_utils import ( decode_hex, is_bytes, @@ -116,58 +128,6 @@ def filter_by_argument_name(argument_names, contract_abi): ] -try: - from eth_abi.abi import ( - process_type, - collapse_type, - ) -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 isinstance(sub, tuple): - sub = 'x'.join(map(str, sub)) - elif isinstance(sub, int): - sub = str(sub) - else: - sub = '' - - arrlist = abi_type.arrlist - if isinstance(arrlist, tuple): - arrlist = list(map(list, arrlist)) - else: - arrlist = [] - - return abi_type.base, sub, arrlist - - def collapse_type(base, sub, arrlist): - return base + str(sub) + ''.join(map(repr, arrlist)) - - class AddressEncoder(encoding.AddressEncoder): @classmethod def validate_value(cls, value): @@ -275,6 +235,15 @@ def check_if_arguments_can_be_encoded(function_abi, args, kwargs): def merge_args_and_kwargs(function_abi, args, kwargs): + """ + Takes a list of positional args (``args``) and a dict of keyword args + (``kwargs``) defining values to be passed to a call to the contract function + described by ``function_abi``. Checks to ensure that the correct number of + args were given, no duplicate args were given, and no unknown args were + given. Returns a list of argument values aligned to the order of inputs + defined in ``function_abi``. + """ + # Ensure the function is being applied to the correct number of args if len(args) + len(kwargs) != len(function_abi.get('inputs', [])): raise TypeError( "Incorrect argument count. Expected '{0}'. Got '{1}'".format( @@ -283,47 +252,50 @@ def merge_args_and_kwargs(function_abi, args, kwargs): ) ) + # If no keyword args were given, we don't need to align them if not kwargs: return args - 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: + kwarg_names = set(kwargs.keys()) + sorted_arg_names = tuple(arg_abi['name'] for arg_abi in function_abi['inputs']) + args_as_kwargs = dict(zip(sorted_arg_names, args)) + + # Check for duplicate args + duplicate_args = kwarg_names.intersection(args_as_kwargs.keys()) + if duplicate_args: raise TypeError( "{fn_name}() got multiple values for argument(s) '{dups}'".format( fn_name=function_abi['name'], - dups=', '.join(duplicate_keys), + dups=', '.join(duplicate_args), ) ) - 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: + # Check for unknown args + unknown_args = kwarg_names.difference(sorted_arg_names) + if unknown_args: if function_abi.get('name'): raise TypeError( "{fn_name}() got unexpected keyword argument(s) '{dups}'".format( fn_name=function_abi.get('name'), - dups=', '.join(unknown_kwargs), + dups=', '.join(unknown_args), ) ) - # show type instead of name in the error message incase key 'name' is missing. raise TypeError( "Type: '{_type}' got unexpected keyword argument(s) '{dups}'".format( _type=function_abi.get('type'), - dups=', '.join(unknown_kwargs), + dups=', '.join(unknown_args), ) ) - sorted_args = list(zip( + # Sort args according to their position in the ABI and unzip them from their + # names + sorted_args = tuple(zip( *sorted( itertools.chain(kwargs.items(), args_as_kwargs.items()), - key=lambda kv: sorted_arg_names.index(kv[0]) + key=lambda kv: sorted_arg_names.index(kv[0]), ) )) + if sorted_args: return sorted_args[1] else: @@ -585,7 +557,7 @@ class ABITypedData(namedtuple('ABITypedData', 'abi_type, data')): >>> 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: @@ -601,28 +573,18 @@ def __new__(cls, iterable): return super().__new__(cls, *iterable) -def abi_sub_tree(data_type, data_value): - if data_type is None: +def abi_sub_tree(abi_type: Optional[Union[TypeStr, ABIType]], data_value: Any) -> ABITypedData: + if abi_type is None: return ABITypedData([None, data_value]) - try: - base, sub, arrlist = data_type - except ValueError: - base, sub, arrlist = process_type(data_type) - - collapsed = collapse_type(base, sub, arrlist) - - if arrlist: - sub_type = (base, sub, arrlist[:-1]) - return ABITypedData([ - collapsed, - [ - abi_sub_tree(sub_type, sub_value) - for sub_value in data_value - ], - ]) + if isinstance(abi_type, TypeStr): + abi_type = parse(abi_type) + + if abi_type.is_array: + it = abi_type.item_type + return ABITypedData([abi_type.to_type_str(), [abi_sub_tree(it, i) for i in data_value]]) else: - return ABITypedData([collapsed, data_value]) + return ABITypedData([abi_type.to_type_str(), data_value]) def strip_abi_type(elements): diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index 997ca6a8fd..84862d123a 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -3,9 +3,6 @@ from eth_abi import ( encode_abi as eth_abi_encode_abi, ) -from eth_abi.exceptions import ( - EncodingError, -) from eth_utils import ( add_0x_prefix, encode_hex, @@ -77,7 +74,6 @@ def find_matching_event_abi(abi, event_name=None, argument_names=None): def find_matching_fn_abi(abi, fn_identifier=None, args=None, kwargs=None): args = args or tuple() kwargs = kwargs or dict() - filters = [] num_arguments = len(args) + len(kwargs) if fn_identifier is FallbackFn: @@ -89,19 +85,18 @@ def find_matching_fn_abi(abi, fn_identifier=None, args=None, kwargs=None): name_filter = functools.partial(filter_by_name, fn_identifier) arg_count_filter = functools.partial(filter_by_argument_count, num_arguments) encoding_filter = functools.partial(filter_by_encodability, args, kwargs) - filters.extend([ - name_filter, - arg_count_filter, - encoding_filter, - ]) - function_candidates = pipe(abi, *filters) + + function_candidates = pipe(abi, name_filter, arg_count_filter, encoding_filter) + if len(function_candidates) == 1: return function_candidates[0] else: matching_identifiers = name_filter(abi) matching_function_signatures = [abi_to_signature(func) for func in matching_identifiers] + arg_count_matches = len(arg_count_filter(matching_identifiers)) encoding_matches = len(encoding_filter(matching_identifiers)) + if arg_count_matches == 0: diagnosis = "\nFunction invocation failed due to improper number of arguments." elif encoding_matches == 0: @@ -111,6 +106,7 @@ def find_matching_fn_abi(abi, fn_identifier=None, args=None, kwargs=None): "\nAmbiguous argument encoding. " "Provided arguments can be encoded to multiple functions matching this call." ) + message = ( "\nCould not identify the intended function with name `{name}`, " "positional argument(s) of type `{arg_types}` and " @@ -125,6 +121,7 @@ def find_matching_fn_abi(abi, fn_identifier=None, args=None, kwargs=None): candidates=matching_function_signatures, diagnosis=diagnosis, ) + raise ValidationError(message) @@ -139,27 +136,21 @@ def encode_abi(web3, abi, arguments, data=None): ) ) - try: - normalizers = [ - abi_ens_resolver(web3), - abi_address_to_hex, - abi_bytes_to_bytes, - abi_string_to_text, - ] - normalized_arguments = map_abi_data( - normalizers, - argument_types, - arguments, - ) - encoded_arguments = eth_abi_encode_abi( - argument_types, - normalized_arguments, - ) - except EncodingError as e: - raise TypeError( - "One or more arguments could not be encoded to the necessary " - "ABI type: {0}".format(str(e)) - ) + normalizers = [ + abi_ens_resolver(web3), + abi_address_to_hex, + abi_bytes_to_bytes, + abi_string_to_text, + ] + normalized_arguments = map_abi_data( + normalizers, + argument_types, + arguments, + ) + encoded_arguments = eth_abi_encode_abi( + argument_types, + normalized_arguments, + ) if data: return to_hex(HexBytes(data) + encoded_arguments) diff --git a/web3/_utils/encoding.py b/web3/_utils/encoding.py index ab76ae361c..3e066e2e0b 100644 --- a/web3/_utils/encoding.py +++ b/web3/_utils/encoding.py @@ -19,6 +19,9 @@ remove_0x_prefix, to_hex, ) +from hexbytes import ( + HexBytes, +) from web3._utils.abi import ( is_address_type, @@ -39,6 +42,9 @@ validate_abi_type, validate_abi_value, ) +from web3.datastructures import ( + AttributeDict, +) def hex_encode_abi_type(abi_type, value, force_size=None): @@ -238,9 +244,9 @@ def _json_list_errors(self, iterable): except TypeError as exc: yield "%d: because (%s)" % (index, exc) - def _friendly_json_encode(self, obj): + def _friendly_json_encode(self, obj, cls=None): try: - encoded = json.dumps(obj) + encoded = json.dumps(obj, cls=cls) return encoded except TypeError as full_exception: if hasattr(obj, 'items'): @@ -262,9 +268,9 @@ def json_decode(self, json_str): # so we have to re-raise the same type. raise json.decoder.JSONDecodeError(err_msg, exc.doc, exc.pos) - def json_encode(self, obj): + def json_encode(self, obj, cls=None): try: - return self._friendly_json_encode(obj) + return self._friendly_json_encode(obj, cls=cls) except TypeError as exc: raise TypeError("Could not encode to JSON: {}".format(exc)) @@ -299,7 +305,7 @@ def encode_single_packed(_type, value): from eth_abi.registry import has_arrlist, registry abi_type = abi_type_parser.parse(_type) if has_arrlist(_type): - item_encoder = registry.get_encoder(str(abi_type.item_type)) + item_encoder = registry.get_encoder(abi_type.item_type.to_type_str()) if abi_type.arrlist[-1] != 1: return DynamicArrayPackedEncoder(item_encoder=item_encoder).encode(value) else: @@ -309,3 +315,19 @@ def encode_single_packed(_type, value): return codecs.encode(value, 'utf8') elif abi_type.base == "bytes": return value + + +class Web3JsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, AttributeDict): + return {k: v for k, v in obj.items()} + if isinstance(obj, HexBytes): + return obj.hex() + return json.JSONEncoder.default(self, obj) + + +def to_json(obj): + ''' + Convert a complex object (like a transaction object) to a JSON string + ''' + return FriendlyJsonSerde().json_encode(obj, cls=Web3JsonEncoder) diff --git a/web3/_utils/events.py b/web3/_utils/events.py index eaf260dc86..459459e30e 100644 --- a/web3/_utils/events.py +++ b/web3/_utils/events.py @@ -8,6 +8,10 @@ decode_abi, decode_single, encode_single, + grammar, +) +from eth_typing import ( + TypeStr, ) from eth_utils import ( encode_hex, @@ -51,7 +55,6 @@ get_indexed_event_inputs, map_abi_data, normalize_event_input_types, - process_type, ) @@ -133,15 +136,9 @@ def construct_event_data_set(event_abi, arguments=None): return data -def is_dynamic_sized_type(_type): - base_type, type_size, arrlist = process_type(_type) - if arrlist: - return True - elif base_type == 'string': - return True - elif base_type == 'bytes' and type_size == '': - return True - return False +def is_dynamic_sized_type(type_str: TypeStr) -> bool: + abi_type = grammar.parse(type_str) + return abi_type.is_dynamic @to_tuple diff --git a/web3/_utils/module_testing/parity_module.py b/web3/_utils/module_testing/parity_module.py index 75eeb5f4f5..326d61b617 100644 --- a/web3/_utils/module_testing/parity_module.py +++ b/web3/_utils/module_testing/parity_module.py @@ -91,3 +91,13 @@ def test_trace_filter(self, web3, txn_filter_params, parity_fixture_data): trace = web3.parity.traceFilter(txn_filter_params) assert isinstance(trace, list) assert trace[0]['action']['from'] == add_0x_prefix(parity_fixture_data['coinbase']) + + + @pytest.mark.parametrize( + 'params', + ['enode://f1a6b0bdbf014355587c3018454d070ac57801f05d3b39fe85da574f002a32e929f683d72aa5a8318382e4d3c7a05c9b91687b0d997a39619fb8a6e7ad88e512@1.1.1.1:30300'] + ) + def test_add_reserved_peer(self, web3, params): + response = web3.parity.addReservedPeer([params]) + assert response["result"]==True + diff --git a/web3/_utils/normalizers.py b/web3/_utils/normalizers.py index daf1e8bd78..ab873ca95b 100644 --- a/web3/_utils/normalizers.py +++ b/web3/_utils/normalizers.py @@ -7,6 +7,13 @@ import json import eth_abi +from eth_abi.exceptions import ( + ParseError, +) +from eth_abi.grammar import ( + BasicType, + parse, +) from eth_utils import ( to_checksum_address, ) @@ -17,9 +24,6 @@ HexBytes, ) -from web3._utils.abi import ( - process_type, -) from web3._utils.encoding import ( hexstr_if_str, text_if_str, @@ -46,10 +50,10 @@ def implicitly_identity(to_wrap): @functools.wraps(to_wrap) - def wrapper(abi_type, data): - modified = to_wrap(abi_type, data) + def wrapper(type_str, data): + modified = to_wrap(type_str, data) if modified is None: - return abi_type, data + return type_str, data else: return modified return wrapper @@ -61,15 +65,15 @@ def wrapper(abi_type, data): @implicitly_identity -def addresses_checksummed(abi_type, data): - if abi_type == 'address': - return abi_type, to_checksum_address(data) +def addresses_checksummed(type_str, data): + if type_str == 'address': + return type_str, to_checksum_address(data) @implicitly_identity -def decode_abi_strings(abi_type, data): - if abi_type == 'string': - return abi_type, codecs.decode(data, 'utf8', 'backslashreplace') +def decode_abi_strings(type_str, data): + if type_str == 'string': + return type_str, codecs.decode(data, 'utf8', 'backslashreplace') # @@ -77,63 +81,87 @@ def decode_abi_strings(abi_type, data): # +def parse_basic_type_str(old_normalizer): + """ + Modifies a normalizer to automatically parse the incoming type string. If + that type string does not represent a basic type (i.e. non-tuple type) or is + not parsable, the normalizer does nothing. + """ + @functools.wraps(old_normalizer) + def new_normalizer(type_str, data): + try: + abi_type = parse(type_str) + except ParseError: + # If type string is not parsable, do nothing + return type_str, data + + if not isinstance(abi_type, BasicType): + return type_str, data + + return old_normalizer(abi_type, type_str, data) + + return new_normalizer + + @implicitly_identity -def abi_bytes_to_hex(abi_type, data): - base, sub, arrlist = process_type(abi_type) - if base == 'bytes' and not arrlist: - bytes_data = hexstr_if_str(to_bytes, data) - if not sub: - return abi_type, to_hex(bytes_data) - else: - num_bytes = int(sub) - if len(bytes_data) <= num_bytes: - padded = bytes_data.ljust(num_bytes, b'\0') - return abi_type, to_hex(padded) - else: - raise ValueError( - "This value was expected to be at most %d bytes, but instead was %d: %r" % ( - (num_bytes, len(bytes_data), data) - ) - ) +@parse_basic_type_str +def abi_bytes_to_hex(abi_type, type_str, data): + if abi_type.base != 'bytes' or abi_type.is_array: + return + + bytes_data = hexstr_if_str(to_bytes, data) + if abi_type.sub is None: + return type_str, to_hex(bytes_data) + + num_bytes = abi_type.sub + if len(bytes_data) > num_bytes: + raise ValueError( + "This value was expected to be at most %d bytes, but instead was %d: %r" % ( + (num_bytes, len(bytes_data), data) + ) + ) + + padded = bytes_data.ljust(num_bytes, b'\0') + return type_str, to_hex(padded) @implicitly_identity -def abi_int_to_hex(abi_type, data): - base, _sub, arrlist = process_type(abi_type) - if base == 'uint' and not arrlist: +@parse_basic_type_str +def abi_int_to_hex(abi_type, type_str, data): + if abi_type.base == 'uint' and not abi_type.is_array: return abi_type, hexstr_if_str(to_hex, data) @implicitly_identity -def abi_string_to_hex(abi_type, data): - if abi_type == 'string': - return abi_type, text_if_str(to_hex, data) +def abi_string_to_hex(type_str, data): + if type_str == 'string': + return type_str, text_if_str(to_hex, data) @implicitly_identity -def abi_string_to_text(abi_type, data): - if abi_type == 'string': - return abi_type, text_if_str(to_text, data) +def abi_string_to_text(type_str, data): + if type_str == 'string': + return type_str, text_if_str(to_text, data) @implicitly_identity -def abi_bytes_to_bytes(abi_type, data): - base, sub, arrlist = process_type(abi_type) - if base == 'bytes' and not arrlist: - return abi_type, hexstr_if_str(to_bytes, data) +@parse_basic_type_str +def abi_bytes_to_bytes(abi_type, type_str, data): + if abi_type.base == 'bytes' and not abi_type.is_array: + return type_str, hexstr_if_str(to_bytes, data) @implicitly_identity -def abi_address_to_hex(abi_type, data): - if abi_type == 'address': +def abi_address_to_hex(type_str, data): + if type_str == 'address': validate_address(data) if is_binary_address(data): - return abi_type, to_checksum_address(data) + return type_str, to_checksum_address(data) @curry -def abi_ens_resolver(w3, abi_type, val): - if abi_type == 'address' and is_ens_name(val): +def abi_ens_resolver(w3, type_str, val): + if type_str == 'address' and is_ens_name(val): if w3 is None: raise InvalidAddress( "Could not look up name %r because no web3" @@ -150,9 +178,9 @@ def abi_ens_resolver(w3, abi_type, val): " not connected to mainnet" % (val) ) else: - return (abi_type, validate_name_has_address(w3.ens, val)) + return type_str, validate_name_has_address(w3.ens, val) else: - return (abi_type, val) + return type_str, val BASE_RETURN_NORMALIZERS = [ diff --git a/web3/auto/infura/endpoints.py b/web3/auto/infura/endpoints.py index 3d87bb13cf..072651eb33 100644 --- a/web3/auto/infura/endpoints.py +++ b/web3/auto/infura/endpoints.py @@ -15,27 +15,29 @@ def load_api_key(): - # at web3py v5, drop old variable name INFURA_API_KEY - key = os.environ.get( - 'WEB3_INFURA_API_KEY', - os.environ.get('INFURA_API_KEY', '') - ) + # in web3py v6 remove outdated WEB3_INFURA_API_KEY + key = os.environ.get('WEB3_INFURA_PROJECT_ID', + os.environ.get('WEB3_INFURA_API_KEY', '')) if key == '': logging.getLogger('web3.auto.infura').warning( - "No Infura API Key found. Add environment variable WEB3_INFURA_API_KEY to ensure " - "continued API access. New keys are available at https://infura.io/register" + "No Infura Project ID found. Add environment variable WEB3_INFURA_PROJECT_ID to " + " ensure continued API access after March 27th. " + "New keys are available at https://infura.io/register" ) return key def build_infura_url(domain): scheme = os.environ.get('WEB3_INFURA_SCHEME', WEBSOCKET_SCHEME) - - if scheme == WEBSOCKET_SCHEME: - # websockets doesn't use the API key (yet?) - return "%s://%s/ws" % (scheme, domain) + key = load_api_key() + + if key and scheme == WEBSOCKET_SCHEME: + return "%s://%s/ws/v3/%s" % (scheme, domain, key) + elif key and scheme == HTTP_SCHEME: + return "%s://%s/v3/%s" % (scheme, domain, key) + elif scheme == WEBSOCKET_SCHEME: + return "%s://%s/ws/" % (scheme, domain) elif scheme == HTTP_SCHEME: - key = load_api_key() return "%s://%s/%s" % (scheme, domain, key) else: raise ValidationError("Cannot connect to Infura with scheme %r" % scheme) diff --git a/web3/auto/infura/rinkeby.py b/web3/auto/infura/rinkeby.py index 46c6796b9e..245a4e0743 100644 --- a/web3/auto/infura/rinkeby.py +++ b/web3/auto/infura/rinkeby.py @@ -1,4 +1,7 @@ from web3 import Web3 +from web3.middleware import ( + geth_poa_middleware, +) from web3.providers.auto import ( load_provider_from_uri, ) @@ -11,3 +14,4 @@ _infura_url = build_infura_url(INFURA_RINKEBY_DOMAIN) w3 = Web3(load_provider_from_uri(_infura_url)) +w3.middleware_stack.inject(geth_poa_middleware, layer=0) diff --git a/web3/contract.py b/web3/contract.py index 74622056f4..40a5e332ad 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -2,7 +2,6 @@ """ import copy -import functools import itertools from eth_abi import ( @@ -88,6 +87,7 @@ FallbackNotFound, MismatchedABI, NoABIEventsFound, + NoABIFound, NoABIFunctionsFound, ) @@ -106,8 +106,11 @@ class ContractFunctions: """ def __init__(self, abi, web3, address=None): - if abi: - self.abi = abi + self.abi = abi + self.web3 = web3 + self.address = address + + if self.abi: self._functions = filter_by_type('function', self.abi) for func in self._functions: setattr( @@ -115,9 +118,9 @@ def __init__(self, abi, web3, address=None): func['name'], ContractFunction.factory( func['name'], - web3=web3, + web3=self.web3, contract_abi=self.abi, - address=address, + address=self.address, function_identifier=func['name'])) def __iter__(self): @@ -128,6 +131,10 @@ def __iter__(self): yield func['name'] def __getattr__(self, function_name): + if self.abi is None: + raise NoABIFound( + "There is no ABI found for this contract.", + ) if '_functions' not in self.__dict__: raise NoABIFunctionsFound( "The abi for this contract contains no function definitions. ", @@ -241,6 +248,7 @@ class Contract: clone_bin = None functions = None + caller = None #: Instance of :class:`ContractEvents` presenting available Event ABIs events = None @@ -271,6 +279,7 @@ def __init__(self, address=None): raise TypeError("The address argument is required to instantiate a contract.") self.functions = ContractFunctions(self.abi, self.web3, self.address) + self.caller = ContractCaller(self.abi, self.web3, self.address) self.events = ContractEvents(self.abi, self.web3, self.address) self.fallback = Contract.get_fallback_function(self.abi, self.web3, self.address) @@ -293,6 +302,7 @@ def factory(cls, web3, class_name=None, **kwargs): normalizers=normalizers, ) contract.functions = ContractFunctions(contract.abi, contract.web3) + contract.caller = ContractCaller(contract.abi, contract.web3, contract.address) contract.events = ContractEvents(contract.abi, contract.web3) contract.fallback = Contract.get_fallback_function(contract.abi, contract.web3) @@ -301,59 +311,6 @@ def factory(cls, web3, class_name=None, **kwargs): # # Contract Methods # - @classmethod - @deprecated_for("contract.constructor.transact") - def deploy(cls, transaction=None, args=None, kwargs=None): - """ - Deploys the contract on a blockchain. - - Example: - - .. code-block:: python - - >>> MyContract.deploy( - transaction={ - 'from': web3.eth.accounts[1], - 'value': 12345, - }, - args=('DGD', 18), - ) - '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060' - - :param transaction: Transaction parameters for the deployment - transaction as a dict - - :param args: The contract constructor arguments as positional arguments - :param kwargs: The contract constructor arguments as keyword arguments - - :return: hexadecimal transaction hash of the deployment transaction - """ - if transaction is None: - deploy_transaction = {} - else: - deploy_transaction = dict(**transaction) - - if not cls.bytecode: - raise ValueError( - "Cannot deploy a contract that does not have 'bytecode' associated " - "with it" - ) - - if 'data' in deploy_transaction: - raise ValueError( - "Cannot specify `data` for contract deployment" - ) - - if 'to' in deploy_transaction: - raise ValueError( - "Cannot specify `to` for contract deployment" - ) - - deploy_transaction['data'] = cls._encode_constructor_data(args, kwargs) - - txn_hash = cls.web3.eth.sendTransaction(deploy_transaction) - return txn_hash - @classmethod def constructor(cls, *args, **kwargs): """ @@ -392,255 +349,6 @@ def encodeABI(cls, fn_name, args=None, kwargs=None, data=None): return encode_abi(cls.web3, fn_abi, fn_arguments, data) - @combomethod - @deprecated_for("contract.functions..estimateGas") - def estimateGas(self, transaction=None): - """ - Estimate the gas for a call - """ - if transaction is None: - estimate_transaction = {} - else: - estimate_transaction = dict(**transaction) - - if 'data' in estimate_transaction: - raise ValueError("Cannot set data in call transaction") - if 'to' in estimate_transaction: - raise ValueError("Cannot set to in call transaction") - - if self.address: - estimate_transaction.setdefault('to', self.address) - if self.web3.eth.defaultAccount is not empty: - estimate_transaction.setdefault('from', self.web3.eth.defaultAccount) - - if 'to' not in estimate_transaction: - if isinstance(self, type): - raise ValueError( - "When using `Contract.estimateGas` from a contract factory " - "you must provide a `to` address with the transaction" - ) - else: - raise ValueError( - "Please ensure that this contract instance has an address." - ) - - contract = self - - class Caller: - def __getattr__(self, function_name): - callable_fn = functools.partial( - estimate_gas_for_function, - contract.address, - contract.web3, - function_name, - estimate_transaction, - contract.abi, - None, - ) - return callable_fn - - return Caller() - - @combomethod - @deprecated_for("contract...call") - def call(self, transaction=None): - """ - Execute a contract function call using the `eth_call` interface. - - This method prepares a ``Caller`` object that exposes the contract - functions and public variables as callable Python functions. - - Reading a public ``owner`` address variable example: - - .. code-block:: python - - ContractFactory = w3.eth.contract( - abi=wallet_contract_definition["abi"] - ) - - # Not a real contract address - contract = ContractFactory("0x2f70d3d26829e412A602E83FE8EeBF80255AEeA5") - - # Read "owner" public variable - addr = contract.functions.owner().call() - - :param transaction: Dictionary of transaction info for web3 interface - :return: ``Caller`` object that has contract public functions - and variables exposed as Python methods - """ - if transaction is None: - call_transaction = {} - else: - call_transaction = dict(**transaction) - - if 'data' in call_transaction: - raise ValueError("Cannot set data in call transaction") - - if self.address: - call_transaction.setdefault('to', self.address) - if self.web3.eth.defaultAccount is not empty: - call_transaction.setdefault('from', self.web3.eth.defaultAccount) - - if 'to' not in call_transaction: - if isinstance(self, type): - raise ValueError( - "When using `Contract.call` from a contract factory you " - "must provide a `to` address with the transaction" - ) - else: - raise ValueError( - "Please ensure that this contract instance has an address." - ) - - contract = self - - class Caller: - def __getattr__(self, function_name): - callable_fn = functools.partial( - call_contract_function, - contract.web3, - contract.address, - contract._return_data_normalizers, - function_name, - call_transaction, - 'latest', - contract.abi, - None, - ) - return callable_fn - - return Caller() - - @combomethod - @deprecated_for("contract...transact") - def transact(self, transaction=None): - """ - Execute a contract function call using the `eth_sendTransaction` - interface. - - You should specify the account that pays the gas for this transaction - in `transaction`. If no account is specified the coinbase account of - web3 interface is used. - - Example: - - .. code-block:: python - - # Assume we have a Wallet contract with the following methods. - # * Wallet.deposit() # deposits to `msg.sender` - # * Wallet.deposit(address to) # deposits to the account indicated - # by the `to` parameter. - # * Wallet.withdraw(address amount) - - >>> wallet = Wallet(address='0xDc3A9Db694BCdd55EBaE4A89B22aC6D12b3F0c24') - # Deposit to the `web3.eth.coinbase` account. - >>> wallet.functions.deposit().transact({'value': 12345}) - '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060' - # Deposit to some other account using funds from `web3.eth.coinbase`. - >>> wallet.functions.deposit(web3.eth.accounts[1]).transact({'value': 54321}) - '0xe122ba26d25a93911e241232d3ba7c76f5a6bfe9f8038b66b198977115fb1ddf' - # Withdraw 12345 wei. - >>> wallet.functions.withdraw(12345).transact() - - The new public transaction will be created. Transaction receipt will - be available once the transaction has been mined. - - :param transaction: Dictionary of transaction info for web3 interface. - Variables include ``from``, ``gas``, ``value``, ``gasPrice``, ``nonce``. - - :return: ``Transactor`` object that has contract - public functions exposed as Python methods. - Calling these methods will execute a transaction against the contract. - - """ - if transaction is None: - transact_transaction = {} - else: - transact_transaction = dict(**transaction) - - if 'data' in transact_transaction: - raise ValueError("Cannot set data in call transaction") - - if self.address is not None: - transact_transaction.setdefault('to', self.address) - if self.web3.eth.defaultAccount is not empty: - transact_transaction.setdefault('from', self.web3.eth.defaultAccount) - - if 'to' not in transact_transaction: - if isinstance(self, type): - raise ValueError( - "When using `Contract.transact` from a contract factory you " - "must provide a `to` address with the transaction" - ) - else: - raise ValueError( - "Please ensure that this contract instance has an address." - ) - - contract = self - - class Transactor: - def __getattr__(self, function_name): - callable_fn = functools.partial( - transact_with_contract_function, - contract.address, - contract.web3, - function_name, - transact_transaction, - contract.abi, - None, - ) - return callable_fn - - return Transactor() - - @combomethod - @deprecated_for("contract...buildTransaction") - def buildTransaction(self, transaction=None): - """ - Build the transaction dictionary without sending - """ - if transaction is None: - built_transaction = {} - else: - built_transaction = dict(**transaction) - - if 'data' in built_transaction: - raise ValueError("Cannot set data in call buildTransaction") - - if isinstance(self, type) and 'to' not in built_transaction: - raise ValueError( - "When using `Contract.buildTransaction` from a contract factory " - "you must provide a `to` address with the transaction" - ) - if not isinstance(self, type) and 'to' in built_transaction: - raise ValueError("Cannot set to in call buildTransaction") - - if self.address: - built_transaction.setdefault('to', self.address) - - if 'to' not in built_transaction: - raise ValueError( - "Please ensure that this contract instance has an address." - ) - - contract = self - - class Caller: - def __getattr__(self, function_name): - callable_fn = functools.partial( - build_transaction_for_function, - contract.address, - contract.web3, - function_name, - built_transaction, - contract.abi, - None, - ) - return callable_fn - - return Caller() - @combomethod def all_functions(self): return find_functions_by_identifier( @@ -911,6 +619,9 @@ class ConciseContract: > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) """ + @deprecated_for( + "contract.caller. or contract.caller({transaction_dict})." + ) def __init__(self, classic_contract, method_class=ConciseMethod): classic_contract._return_data_normalizers += CONCISE_NORMALIZERS @@ -962,6 +673,7 @@ def __call_by_default(self, args): return function_abi['constant'] if 'constant' in function_abi.keys() else False + @deprecated_for("classic contract syntax. Ex: contract.functions.withdraw(amount).transact({})") def __call__(self, *args, **kwargs): # Modifier is not provided and method is not constant/pure do a transaction instead if not kwargs and not self.__call_by_default(args): @@ -1442,6 +1154,97 @@ def factory(cls, class_name, **kwargs): return PropertyCheckingFactory(class_name, (cls,), kwargs) +class ContractCaller: + """ + An alternative Contract API. + + This call: + + > contract.caller({'from': eth.accounts[1], 'gas': 100000, ...}).add(2, 3) + is equivalent to this call in the classic contract: + > contract.functions.add(2, 3).call({'from': eth.accounts[1], 'gas': 100000, ...}) + + Other options for invoking this class include: + + > contract.caller.add(2, 3) + + or + + > contract.caller().add(2, 3) + + or + + > contract.caller(transaction={'from': eth.accounts[1], 'gas': 100000, ...}).add(2, 3) + """ + def __init__(self, + abi, + web3, + address, + transaction=None, + block_identifier='latest'): + self.web3 = web3 + self.address = address + self.abi = abi + self._functions = None + + if self.abi: + if transaction is None: + transaction = {} + + self._functions = filter_by_type('function', self.abi) + for func in self._functions: + fn = ContractFunction.factory( + func['name'], + web3=self.web3, + contract_abi=self.abi, + address=self.address, + function_identifier=func['name']) + + block_id = parse_block_identifier(self.web3, block_identifier) + caller_method = partial(self.call_function, + fn, + transaction=transaction, + block_identifier=block_id) + + setattr(self, func['name'], caller_method) + + def __getattr__(self, function_name): + if self.abi is None: + raise NoABIFound( + "There is no ABI found for this contract.", + ) + elif not self._functions or len(self._functions) == 0: + raise NoABIFunctionsFound( + "The ABI for this contract contains no function definitions. ", + "Are you sure you provided the correct contract ABI?" + ) + elif function_name not in self._functions: + functions_available = ', '.join([fn['name'] for fn in self._functions]) + raise MismatchedABI( + "The function '{}' was not found in this contract's ABI. ".format(function_name), + "Here is a list of all of the function names found: ", + "{}. ".format(functions_available), + "Did you mean to call one of those functions?" + ) + else: + return super().__getattribute__(function_name) + + def __call__(self, transaction=None, block_identifier='latest'): + if transaction is None: + transaction = {} + return type(self)(self.abi, + self.web3, + self.address, + transaction=transaction, + block_identifier=block_identifier) + + @staticmethod + def call_function(fn, *args, transaction=None, block_identifier='latest', **kwargs): + if transaction is None: + transaction = {} + return fn(*args, **kwargs).call(transaction, block_identifier) + + def check_for_forbidden_api_filter_arguments(event_abi, _filters): name_indexed_inputs = {_input['name']: _input for _input in event_abi['inputs']} diff --git a/web3/exceptions.py b/web3/exceptions.py index 475d56bdfc..ad760881bf 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -82,7 +82,14 @@ class ValidationError(Exception): class NoABIFunctionsFound(AttributeError): """ - Raised when an ABI doesn't contain any functions. + Raised when an ABI is present, but doesn't contain any functions. + """ + pass + + +class NoABIFound(AttributeError): + """ + Raised when no ABI is present. """ pass @@ -114,3 +121,10 @@ class PMError(Exception): Raised when an error occurs in the PM module. """ pass + + +class ManifestValidationError(PMError): + """ + Raised when a provided manifest cannot be published, since it's invalid. + """ + pass diff --git a/web3/main.py b/web3/main.py index 2210c46ee4..81e48eac49 100644 --- a/web3/main.py +++ b/web3/main.py @@ -29,6 +29,7 @@ to_hex, to_int, to_text, + to_json, ) from web3._utils.normalizers import ( abi_ens_resolver, @@ -112,6 +113,7 @@ class Web3: toInt = staticmethod(to_int) toHex = staticmethod(to_hex) toText = staticmethod(to_text) + toJSON = staticmethod(to_json) # Currency Utility toWei = staticmethod(to_wei) @@ -203,7 +205,7 @@ def ens(self, new_ens): @property def pm(self): - if self._pm is not None: + if hasattr(self, '_pm'): return self._pm else: raise AttributeError( diff --git a/web3/parity.py b/web3/parity.py index 6078cda348..36edc90f61 100644 --- a/web3/parity.py +++ b/web3/parity.py @@ -36,6 +36,12 @@ def netPeers(self): [], ) + def addReservedPeer(self, params): + return self.web3.manager.request_blocking( + "parity_addReservedPeer", + params, + ) + def traceReplayTransaction(self, transaction_hash, mode=['trace']): return self.web3.manager.request_blocking( "trace_replayTransaction", diff --git a/web3/pm.py b/web3/pm.py index 006a626feb..e0b79fe962 100644 --- a/web3/pm.py +++ b/web3/pm.py @@ -17,6 +17,7 @@ is_checksum_address, to_bytes, to_canonical_address, + to_checksum_address, to_text, to_tuple, ) @@ -32,6 +33,19 @@ Address, Manifest, ) +from ethpm.utils.backend import ( + resolve_uri_contents, +) +from ethpm.utils.ipfs import ( + is_ipfs_uri, +) +from ethpm.utils.manifest_validation import ( + validate_manifest_against_schema, + validate_raw_manifest_format, +) +from ethpm.utils.uri import ( + is_valid_content_addressed_github_uri, +) from ethpm.validation import ( validate_package_name, validate_package_version, @@ -43,6 +57,7 @@ ) from web3.exceptions import ( InvalidAddress, + ManifestValidationError, NameNotFound, PMError, ) @@ -208,7 +223,7 @@ def __init__(self, address: Address, w3: Web3) -> None: # todo: validate runtime bytecode abi = get_vyper_registry_manifest()["contract_types"]["registry"]["abi"] self.registry = w3.eth.contract(address=address, abi=abi) - self.address = address + self.address = to_checksum_address(address) self.w3 = w3 @classmethod @@ -318,7 +333,7 @@ def __init__(self, address: Address, w3: Web3) -> None: "abi" ] self.registry = w3.eth.contract(address=address, abi=abi) - self.address = address + self.address = to_checksum_address(address) self.w3 = w3 def _release(self, package_name: str, version: str, manifest_uri: str) -> bytes: @@ -451,7 +466,7 @@ def deploy_and_set_registry(self) -> Address: w3.ens.setup_address(ens_name, w3.pm.registry.address) """ self.registry = VyperReferenceRegistry.deploy_new_instance(self.web3) - return self.registry.address + return to_checksum_address(self.registry.address) def release_package( self, package_name: str, version: str, manifest_uri: str @@ -462,12 +477,28 @@ def release_package( to be the registry owner. * Parameters: - * ``package_name``: Must be a valid package name. - * ``version``: Must be a valid package version. - * ``manifest_uri``: Must be a valid manifest URI. - """ - validate_package_name(package_name) - validate_package_version(version) + * ``package_name``: Must be a valid package name, matching the given manifest. + * ``version``: Must be a valid package version, matching the given manifest. + * ``manifest_uri``: Must be a valid content-addressed URI. Currently, only IPFS + and Github content-addressed URIs are supported. + """ + validate_is_supported_manifest_uri(manifest_uri) + raw_manifest = to_text(resolve_uri_contents(manifest_uri)) + validate_raw_manifest_format(raw_manifest) + manifest = json.loads(raw_manifest) + validate_manifest_against_schema(manifest) + if package_name != manifest['package_name']: + raise ManifestValidationError( + f"Provided package name: {package_name} does not match the package name " + f"found in the manifest: {manifest['package_name']}." + ) + + if version != manifest['version']: + raise ManifestValidationError( + f"Provided package version: {version} does not match the package version " + f"found in the manifest: {manifest['version']}." + ) + self._validate_set_registry() return self.registry._release(package_name, version, manifest_uri) @@ -589,13 +620,21 @@ def _validate_set_ens(self) -> None: def get_vyper_registry_manifest() -> Dict[str, Any]: - return json.loads((ASSETS_DIR / "vyper_registry" / "1.0.0.json").read_text()) + return json.loads((ASSETS_DIR / "vyper_registry" / "0.1.0.json").read_text()) def get_solidity_registry_manifest() -> Dict[str, Any]: return json.loads((ASSETS_DIR / "registry" / "1.0.0.json").read_text()) +def validate_is_supported_manifest_uri(uri): + if not is_ipfs_uri(uri) and not is_valid_content_addressed_github_uri(uri): + raise ManifestValidationError( + f"URI: {uri} is not a valid content-addressed URI. " + "Currently only IPFS and Github content-addressed URIs are supported." + ) + + @to_tuple def process_vyper_args(*args: List[str]) -> Iterable[bytes]: for arg in args: