From a256494ec8caa1a0b89cdf16e10296804ce83f23 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Mon, 25 Mar 2019 18:25:41 -0500 Subject: [PATCH] Python contract wrappers (#1721) --- python-packages/cmd_pkgs_in_dep_order.py | 1 + .../contract_wrappers/.discharge.json | 13 + python-packages/contract_wrappers/.pylintrc | 4 + python-packages/contract_wrappers/README.md | 45 +++ python-packages/contract_wrappers/setup.py | 226 +++++++++++ python-packages/contract_wrappers/src/conf.py | 56 +++ .../contract_wrappers/src/doc_static/.gitkeep | 0 .../src/doc_templates/.gitkeep | 0 .../contract_wrappers/src/index.rst | 40 ++ .../contract_wrappers/src/zero_ex/__init__.py | 2 + .../src/zero_ex/contract_wrappers/__init__.py | 352 ++++++++++++++++++ .../_base_contract_wrapper.py | 129 +++++++ .../contract_wrappers/erc20_wrapper.py | 224 +++++++++++ .../contract_wrappers/exchange_wrapper.py | 324 ++++++++++++++++ .../src/zero_ex/contract_wrappers/py.typed | 0 .../zero_ex/contract_wrappers/tx_params.py | 39 ++ .../stubs/distutils/__init__.pyi | 0 .../stubs/distutils/command/__init__.pyi | 0 .../stubs/distutils/command/clean.pyi | 7 + .../stubs/eth_account/__init__.pyi | 1 + .../stubs/eth_account/local.pyi | 3 + .../stubs/eth_utils/__init__.pyi | 3 + .../stubs/hexbytes/__init__.pyi | 1 + .../stubs/pytest/__init__.pyi | 9 + .../stubs/setuptools/__init__.pyi | 8 + .../stubs/setuptools/command/__init__.pyi | 0 .../stubs/setuptools/command/test.pyi | 3 + .../contract_wrappers/stubs/sha3/__init__.pyi | 0 .../contract_wrappers/stubs/web3/__init__.pyi | 56 +++ .../stubs/web3/datastructures.pyi | 5 + .../stubs/web3/exceptions.pyi | 2 + .../stubs/web3/providers/__init__.pyi | 0 .../stubs/web3/providers/base.pyi | 2 + .../stubs/web3/utils/__init__.pyi | 0 .../stubs/web3/utils/datatypes.pyi | 3 + .../contract_wrappers/test/__init__.py | 1 + .../contract_wrappers/test/conftest.py | 79 ++++ .../test/test_base_contract_wrapper.py | 47 +++ .../test/test_erc20_wrapper.py | 80 ++++ .../test/test_exchange_wrapper.py | 124 ++++++ python-packages/contract_wrappers/tox.ini | 25 ++ 41 files changed, 1914 insertions(+) create mode 100644 python-packages/contract_wrappers/.discharge.json create mode 100644 python-packages/contract_wrappers/.pylintrc create mode 100644 python-packages/contract_wrappers/README.md create mode 100755 python-packages/contract_wrappers/setup.py create mode 100644 python-packages/contract_wrappers/src/conf.py create mode 100644 python-packages/contract_wrappers/src/doc_static/.gitkeep create mode 100644 python-packages/contract_wrappers/src/doc_templates/.gitkeep create mode 100644 python-packages/contract_wrappers/src/index.rst create mode 100644 python-packages/contract_wrappers/src/zero_ex/__init__.py create mode 100644 python-packages/contract_wrappers/src/zero_ex/contract_wrappers/__init__.py create mode 100644 python-packages/contract_wrappers/src/zero_ex/contract_wrappers/_base_contract_wrapper.py create mode 100644 python-packages/contract_wrappers/src/zero_ex/contract_wrappers/erc20_wrapper.py create mode 100644 python-packages/contract_wrappers/src/zero_ex/contract_wrappers/exchange_wrapper.py create mode 100644 python-packages/contract_wrappers/src/zero_ex/contract_wrappers/py.typed create mode 100644 python-packages/contract_wrappers/src/zero_ex/contract_wrappers/tx_params.py create mode 100644 python-packages/contract_wrappers/stubs/distutils/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/distutils/command/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/distutils/command/clean.pyi create mode 100644 python-packages/contract_wrappers/stubs/eth_account/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/eth_account/local.pyi create mode 100644 python-packages/contract_wrappers/stubs/eth_utils/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/hexbytes/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/pytest/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/setuptools/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/setuptools/command/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/setuptools/command/test.pyi create mode 100644 python-packages/contract_wrappers/stubs/sha3/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/datastructures.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/exceptions.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/providers/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/providers/base.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/utils/__init__.pyi create mode 100644 python-packages/contract_wrappers/stubs/web3/utils/datatypes.pyi create mode 100644 python-packages/contract_wrappers/test/__init__.py create mode 100644 python-packages/contract_wrappers/test/conftest.py create mode 100644 python-packages/contract_wrappers/test/test_base_contract_wrapper.py create mode 100644 python-packages/contract_wrappers/test/test_erc20_wrapper.py create mode 100644 python-packages/contract_wrappers/test/test_exchange_wrapper.py create mode 100644 python-packages/contract_wrappers/tox.ini diff --git a/python-packages/cmd_pkgs_in_dep_order.py b/python-packages/cmd_pkgs_in_dep_order.py index a56e95bac3..0a989fb1c7 100755 --- a/python-packages/cmd_pkgs_in_dep_order.py +++ b/python-packages/cmd_pkgs_in_dep_order.py @@ -12,6 +12,7 @@ PACKAGE_DEPENDENCY_LIST = [ # independent first) in order for them to resolve properly. "contract_addresses", "contract_artifacts", + "contract_wrappers", "json_schemas", "sra_client", "order_utils", diff --git a/python-packages/contract_wrappers/.discharge.json b/python-packages/contract_wrappers/.discharge.json new file mode 100644 index 0000000000..62c1e576ae --- /dev/null +++ b/python-packages/contract_wrappers/.discharge.json @@ -0,0 +1,13 @@ +{ + "domain": "0x-contract-wrappers-py", + "build_command": "python setup.py build_sphinx", + "upload_directory": "build/docs/html", + "index_key": "index.html", + "error_key": "index.html", + "trailing_slashes": true, + "cache": 3600, + "aws_profile": "default", + "aws_region": "us-east-1", + "cdn": false, + "dns_configured": true +} diff --git a/python-packages/contract_wrappers/.pylintrc b/python-packages/contract_wrappers/.pylintrc new file mode 100644 index 0000000000..85011fdcfe --- /dev/null +++ b/python-packages/contract_wrappers/.pylintrc @@ -0,0 +1,4 @@ +[MESSAGES CONTROL] +disable=C0330,line-too-long,fixme,too-few-public-methods,too-many-ancestors,too-many-arguments +# C0330 is "bad hanging indent". we use indents per `black`. +min-similarity-lines=10 diff --git a/python-packages/contract_wrappers/README.md b/python-packages/contract_wrappers/README.md new file mode 100644 index 0000000000..cba99b9939 --- /dev/null +++ b/python-packages/contract_wrappers/README.md @@ -0,0 +1,45 @@ +## 0x-contract-wrappers + +0x contract wrappers for those developing on top of 0x protocol. + +Read the [documentation](http://0x-contract-wrappers-py.s3-website-us-east-1.amazonaws.com/) + +## Installing + +```bash +pip install 0x-contract-wrappers +``` + +## Contributing + +We welcome improvements and fixes from the wider community! To report bugs within this package, please create an issue in this repository. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started. + +### Install Code and Dependencies + +Ensure that you have installed Python >=3.6 and Docker. Then: + +```bash +pip install -e .[dev] +``` + +### Test + +Tests depend on a running ganache instance and with the 0x contracts deployed in it. For convenience, a docker container is provided that has ganache-cli and a snapshot containing the necessary contracts. A shortcut is provided to run that docker container: `./setup.py ganache`. With that running, the tests can be run with `./setup.py test`. + +### Clean + +`./setup.py clean --all` + +### Lint + +`./setup.py lint` + +### Build Documentation + +`./setup.py build_sphinx` + +### More + +See `./setup.py --help-commands` for more info. diff --git a/python-packages/contract_wrappers/setup.py b/python-packages/contract_wrappers/setup.py new file mode 100755 index 0000000000..55551a7c87 --- /dev/null +++ b/python-packages/contract_wrappers/setup.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +"""setuptools module for contract_wrappers package.""" + +import subprocess # nosec +from shutil import rmtree +from os import environ, path +from pathlib import Path +from sys import argv + +from distutils.command.clean import clean +import distutils.command.build_py +from setuptools import find_packages, setup +from setuptools.command.test import test as TestCommand + + +class TestCommandExtension(TestCommand): + """Run pytest tests.""" + + def run_tests(self): + """Invoke pytest.""" + import pytest + + exit(pytest.main(["--doctest-modules"])) + + +class LintCommand(distutils.command.build_py.build_py): + """Custom setuptools command class for running linters.""" + + description = "Run linters" + + def run(self): + """Run linter shell commands.""" + lint_commands = [ + # formatter: + "black --line-length 79 --check --diff src test setup.py".split(), + # style guide checker (formerly pep8): + "pycodestyle src test setup.py".split(), + # docstring style checker: + "pydocstyle src test setup.py".split(), + # static type checker: + "mypy src test setup.py".split(), + # security issue checker: + "bandit -r src ./setup.py".split(), + # general linter: + "pylint src test setup.py".split(), + # pylint takes relatively long to run, so it runs last, to enable + # fast failures. + ] + + # tell mypy where to find interface stubs for 3rd party libs + environ["MYPYPATH"] = path.join( + path.dirname(path.realpath(argv[0])), "stubs" + ) + + # HACK(gene): until eth_utils fixes + # https://github.com/ethereum/eth-utils/issues/140 , we need to simply + # create an empty file `py.typed` in the eth_abi package directory. + import eth_utils + + eth_utils_dir = path.dirname(path.realpath(eth_utils.__file__)) + Path(path.join(eth_utils_dir, "py.typed")).touch() + + for lint_command in lint_commands: + print( + "Running lint command `", " ".join(lint_command).strip(), "`" + ) + subprocess.check_call(lint_command) # nosec + + +class CleanCommandExtension(clean): + """Custom command to do custom cleanup.""" + + def run(self): + """Run the regular clean, followed by our custom commands.""" + super().run() + rmtree("dist", ignore_errors=True) + rmtree(".mypy_cache", ignore_errors=True) + rmtree(".tox", ignore_errors=True) + rmtree(".pytest_cache", ignore_errors=True) + rmtree("src/0x_contract_wrappers.egg-info", ignore_errors=True) + + +class TestPublishCommand(distutils.command.build_py.build_py): + """Custom command to publish to test.pypi.org.""" + + description = ( + "Publish dist/* to test.pypi.org. Run sdist & bdist_wheel first." + ) + + def run(self): + """Run twine to upload to test.pypi.org.""" + subprocess.check_call( # nosec + ( + "twine upload --repository-url https://test.pypi.org/legacy/" + + " --verbose dist/*" + ).split() + ) + + +class PublishCommand(distutils.command.build_py.build_py): + """Custom command to publish to pypi.org.""" + + description = "Publish dist/* to pypi.org. Run sdist & bdist_wheel first." + + def run(self): + """Run twine to upload to pypi.org.""" + subprocess.check_call("twine upload dist/*".split()) # nosec + + +class PublishDocsCommand(distutils.command.build_py.build_py): + """Custom command to publish docs to S3.""" + + description = ( + "Publish docs to " + + "http://0x-contract-wrappers-py.s3-website-us-east-1.amazonaws.com/" + ) + + def run(self): + """Run npm package `discharge` to build & upload docs.""" + subprocess.check_call("discharge deploy".split()) # nosec + + +class GanacheCommand(distutils.command.build_py.build_py): + """Custom command to publish to pypi.org.""" + + description = "Run ganache daemon to support tests." + + def run(self): + """Run ganache.""" + cmd_line = ( + "docker run -d -p 8545:8545 0xorg/ganache-cli:2.2.2" + ).split() + subprocess.call(cmd_line) # nosec + + +with open("README.md", "r") as file_handle: + README_MD = file_handle.read() + + +setup( + name="0x-contract-wrappers", + version="1.0.1", + description="Python wrappers for 0x smart contracts", + long_description=README_MD, + long_description_content_type="text/markdown", + url=( + "https://github.com/0xproject/0x-monorepo/tree/development" + + "/python-packages/contract_wrappers" + ), + author="F. Eugene Aumson", + author_email="feuGeneA@users.noreply.github.com", + cmdclass={ + "clean": CleanCommandExtension, + "lint": LintCommand, + "test": TestCommandExtension, + "test_publish": TestPublishCommand, + "publish": PublishCommand, + "publish_docs": PublishDocsCommand, + "ganache": GanacheCommand, + }, + install_requires=[ + "0x-contract-addresses", + "0x-contract-artifacts", + "0x-json-schemas", + "0x-order-utils", + "0x-web3", + "attrs", + "eth_utils", + "hypothesis>=3.31.2", # HACK! this is web3's dependency! + # above works around https://github.com/ethereum/web3.py/issues/1179 + "mypy_extensions", + ], + extras_require={ + "dev": [ + "bandit", + "black", + "coverage", + "coveralls", + "mypy", + "mypy_extensions", + "pycodestyle", + "pydocstyle", + "pylint", + "pytest", + "sphinx", + "sphinx-autodoc-typehints", + "tox", + "twine", + ] + }, + python_requires=">=3.6, <4", + package_data={"zero_ex.contract_wrappers": ["py.typed"]}, + package_dir={"": "src"}, + license="Apache 2.0", + keywords=( + "ethereum cryptocurrency 0x decentralized blockchain dex exchange" + ), + namespace_packages=["zero_ex"], + packages=find_packages("src"), + classifiers=[ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Financial and Insurance Industry", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Office/Business :: Financial", + "Topic :: Other/Nonlisted Topic", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + ], + zip_safe=False, # required per mypy + command_options={ + "build_sphinx": { + "source_dir": ("setup.py", "src"), + "build_dir": ("setup.py", "build/docs"), + } + }, +) diff --git a/python-packages/contract_wrappers/src/conf.py b/python-packages/contract_wrappers/src/conf.py new file mode 100644 index 0000000000..38c0695294 --- /dev/null +++ b/python-packages/contract_wrappers/src/conf.py @@ -0,0 +1,56 @@ +"""Configuration file for the Sphinx documentation builder.""" + +# Reference: http://www.sphinx-doc.org/en/master/config + +from typing import List + +import pkg_resources + + +# pylint: disable=invalid-name +# because these variables are not named in upper case, as globals should be. + +project = "0x-contract-wrappers" +# pylint: disable=redefined-builtin +copyright = "2019, ZeroEx, Intl." +author = "Michael Huang" +version = pkg_resources.get_distribution("0x-contract-wrappers").version +release = "" # The full version, including alpha/beta/rc tags + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx_autodoc_typehints", +] + +templates_path = ["doc_templates"] + +source_suffix = ".rst" +# eg: source_suffix = [".rst", ".md"] + +master_doc = "index" # The master toctree document. + +language = None + +exclude_patterns: List[str] = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +html_theme = "alabaster" + +html_static_path = ["doc_static"] +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". + +# Output file base name for HTML help builder. +htmlhelp_basename = "contract_wrapperspydoc" + +# -- Extension configuration: + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/python-packages/contract_wrappers/src/doc_static/.gitkeep b/python-packages/contract_wrappers/src/doc_static/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/src/doc_templates/.gitkeep b/python-packages/contract_wrappers/src/doc_templates/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/src/index.rst b/python-packages/contract_wrappers/src/index.rst new file mode 100644 index 0000000000..38750dbb12 --- /dev/null +++ b/python-packages/contract_wrappers/src/index.rst @@ -0,0 +1,40 @@ +Python zero_ex.contract_wrappers +================================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. automodule:: zero_ex.contract_wrappers + :members: + :inherited-members: + + +zero_ex.contract_wrappers.Exchange +---------------------------------- + +.. autoclass:: zero_ex.contract_wrappers.Exchange + :members: + :inherited-members: + + +zero_ex.contract_wrappers.ERC20Token +------------------------------------- + +.. autoclass:: zero_ex.contract_wrappers.ERC20Token + :members: + :inherited-members: + + +zero_ex.contract_wrappers.TxParams +---------------------------------- + +.. autoclass:: zero_ex.contract_wrappers.TxParams + :members: + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/python-packages/contract_wrappers/src/zero_ex/__init__.py b/python-packages/contract_wrappers/src/zero_ex/__init__.py new file mode 100644 index 0000000000..e90d833db6 --- /dev/null +++ b/python-packages/contract_wrappers/src/zero_ex/__init__.py @@ -0,0 +1,2 @@ +"""0x Python API.""" +__import__("pkg_resources").declare_namespace(__name__) diff --git a/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/__init__.py b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/__init__.py new file mode 100644 index 0000000000..b34f070df4 --- /dev/null +++ b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/__init__.py @@ -0,0 +1,352 @@ +"""Python wrappers for interacting with 0x smart contracts. + +The smart contract wrappers have simplified interfaces, +and perform client-side validation on transactions and throw +helpful error messages. + +Installing +========== +Install the 0x-contract-wrappers with pip: + +``pip install 0x-contract-wrappers`` + +Demo +==== +We will demonstrate some basic steps to help you get started trading on 0x. + +Importing packages +------------------ + +The first step to interact with the 0x smart contract is to import +the following relevant packages: + +>>> import random +>>> from eth_utils import to_checksum_address +>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId +>>> from zero_ex.contract_wrappers import ( +... ERC20Token, Exchange, TxParams +... ) +>>> from zero_ex.order_utils import( +... sign_hash, generate_order_hash_hex) + +Provider +-------- + +We need a web3 provider to allow us to talk to the blockchain. You can +read more about providers +`here `__. In our +case, we are using our local node (ganache), we will connect to our provider +at http://localhost:8545. + +>>> from web3 import HTTPProvider, Web3 +>>> provider = HTTPProvider("http://localhost:8545") +>>> # Create a web3 instance from the provider +>>> web3_instance = Web3(provider) + +Declaring Decimals and Addresses +--------------------------------- + +Since we are dealing with a few contracts, we will specify them now to +reduce the syntax load. Fortunately for us, the 0x python packages comes +with a couple of contract addresses that can be useful to have on hand. +One thing that is important to remember is that there are no decimals in +the Ethereum virtual machine (EVM), which means you always need to keep +track of how many "decimals" each token possesses. Since we will sell some +ZRX for some ETH and since they both have 18 decimals, we can use a shared +constant. Let us first get the addresses of the WETH and ZRX tokens on +the test network Ganache: + +>>> weth_address = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token +>>> zrx_address = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].zrx_token + +Approvals and WETH Balance +-------------------------- + +To trade on 0x, the participants (maker and taker) require a small +amount of initial set up. They need to approve the 0x smart contracts +to move funds on their behalf. In order to give 0x protocol smart contract +access to funds, we need to set allowances (you can read about allowances +`here `__). +In our demo the taker asset is WETH (or Wrapped ETH, you can read about WETH +`here `__)., +as ETH is not an ERC20 token it must first be converted into WETH to be +used by 0x. Concretely, "converting" ETH to WETH means that we will deposit +some ETH in a smart contract acting as a ERC20 wrapper. In exchange of +depositing ETH, we will get some ERC20 compliant tokens called WETH at a +1:1 conversion rate. For example, depositing 10 ETH will give us back 10 WETH +and we can revert the process at any time. + +In this demo, we will use test accounts on Ganache, which are accessible +through the Web3 instance. The first account will be the maker, and the second +account will be the taker. + +>>> import pprint +>>> # Instantiate an instance of the erc20_wrapper with the provider +>>> erc20_wrapper = ERC20Token(provider) +>>> # Get accounts from the web 3 instance +>>> accounts = web3_instance.eth.accounts +>>> pprint.pprint(accounts) +['0x5409ED021D9299bf6814279A6A1411A7e866A631', + '0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb', + '0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84', + '0xE834EC434DABA538cd1b9Fe1582052B880BD7e63', + '0x78dc5D2D739606d31509C31d654056A45185ECb6', + '0xA8dDa8d7F5310E4A9E24F8eBA77E091Ac264f872', + '0x06cEf8E666768cC40Cc78CF93d9611019dDcB628', + '0x4404ac8bd8F9618D27Ad2f1485AA1B2cFD82482D', + '0x7457d5E02197480Db681D3fdF256c7acA21bDc12', + '0x91c987bf62D25945dB517BDAa840A6c661374402'] + +>>> maker = accounts[0] +>>> taker = accounts[1] + +Now we need to allow the 0x ERC20 Proxy to move WETH on behalf of our +maker and taker accounts. Let's let our maker and taker here approve +the 0x ERC20 Proxy an allowance of 100 WETH. + +>>> # Multiplying by 10 ** 18 to account for decimals +>>> ALLOWANCE = (100) * 10 ** 18 +>>> erc20_proxy = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].erc20_proxy + +>>> # Set allowance to the erc20_proxy from maker account +>>> tx = erc20_wrapper.approve( +... weth_address, +... erc20_proxy, +... ALLOWANCE, +... tx_params=TxParams(from_=maker), +... ) +>>> # Check the allowance given to the 0x ERC20 Proxy +>>> maker_allowance = erc20_wrapper.allowance( +... weth_address, +... maker, +... erc20_proxy, +... ) +>>> (maker_allowance) // 10 ** 18 +100 + +>>> # Set allowance to the erc20_proxy from taker account +>>> tx = erc20_wrapper.approve( +... weth_address, +... erc20_proxy, +... ALLOWANCE, +... tx_params=TxParams(from_=taker), +... ) +>>> # Check the allowance given to the 0x ERC20 Proxy +>>> taker_allowance = erc20_wrapper.allowance( +... weth_address, +... taker, +... erc20_proxy, +... ) +>>> (taker_allowance) // 10 ** 18 +100 + +To give our accounts some initial WETH balance, we'll need +to *wrap* some ETH to get WETH. The WETH token contract +contains two extra methods, not included in the ERC20 token +standard, so we will grab the ABI for the WETH Token contract +and call the deposit method to wrap our ETH. Here is how we do so. + +>>> from zero_ex.contract_artifacts import abi_by_name +>>> # Converting 0.5 ETH to base unit wei +>>> deposit_amount = int(0.5 * 10 ** 18) + +>>> # Let's have our maker wrap 1 ETH for 1 WETH +>>> tx = erc20_wrapper.execute_method( +... address=weth_address, +... abi=abi_by_name("WETH9"), +... method="deposit", +... tx_params=TxParams(from_=maker, value=deposit_amount)) +>>> # Checking our maker's WETH balance +>>> maker_balance = erc20_wrapper.balance_of( +... token_address=weth_address, owner_address=maker) +>>> (maker_balance) / 10 ** 18 # doctest: +SKIP +0.5 + +>>> # Let's have our taker wrap 0.5 ETH as well +>>> tx = erc20_wrapper.execute_method( +... address=weth_address, +... abi=abi_by_name("WETH9"), +... method="deposit", +... tx_params=TxParams(from_=taker, value=deposit_amount)) +>>> # Checking our taker's WETH balance +>>> taker_balance = erc20_wrapper.balance_of( +... token_address=weth_address, owner_address=taker) +>>> (taker_balance) / 10 ** 18 # doctest: +SKIP +0.5 + +Now we can trade our WETH tokens on 0x! + +Signing an order +---------------- + +Here is an example of a JSON order previously generated by our maker +to sell 0.1 WETH. To confirm his intent to sell and recieve the described +token amounts in this order, our maker must first sign the order by +creating a signature with the given order data. + +>>> maker +'0x5409ED021D9299bf6814279A6A1411A7e866A631' + +>>> example_order = { +... 'makerAddress': '0x5409ed021d9299bf6814279a6a1411a7e866a631', +... 'takerAddress': '0x0000000000000000000000000000000000000000', +... 'senderAddress': '0x0000000000000000000000000000000000000000', +... 'exchangeAddress': '0x48bacb9266a570d521063ef5dd96e61686dbe788', +... 'feeRecipientAddress': '0x0000000000000000000000000000000000000000', +... 'makerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'takerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'salt': random.randint(1, 100000000000000000), +... 'makerFee': 0, +... 'takerFee': 0, +... 'makerAssetAmount': 100000000000000000, +... 'takerAssetAmount': 100000000000000000, +... 'expirationTimeSeconds': 999999999999999999999} + +Please checkout our demo `here +`__ +if you would like to see how you can create an 0x order +with our python packages. + +To sign this order, we first need to generate the order hash. + +>>> order_hash = generate_order_hash_hex( +... example_order, example_order["exchangeAddress"]) + +Now our maker can sign this order hash with our web3 provider and +the `sign_hash` function from the order utils package. + +>>> maker_signature = sign_hash( +... provider, to_checksum_address(maker), order_hash) + +Now our maker can either deliver his signature and example order +directly to the taker, or he can choose to broadcast the order +with his signature to a 0x-relayer. + +Filling an order +---------------- + +We finally have a valid order! We can now have our taker try +to fill the example order. The *takerAssetAmount* is simply the +amount of tokens (in our case WETH) the taker wants to fill. +For this demonstration, we will be completely filling the order. +Orders may also be partially filled. + +Now let's fill the example order: + +>>> # Instantiate an instance of the exchange_wrapper with +>>> # the provider +>>> zero_ex_exchange = Exchange(provider) +>>> tx_hash = zero_ex_exchange.fill_order( +... order=example_order, +... taker_amount=example_order["takerAssetAmount"], +... signature=maker_signature, +... tx_params=TxParams(from_=taker)) + +Once the transaction is mined, we can get the details of +our exchange through the exchange wrapper. + +>>> fill_event = zero_ex_exchange.get_fill_event(tx_hash) +>>> taker_filled_amount = fill_event[0].args.takerAssetFilledAmount +>>> taker_filled_amount / 10 ** 18 +0.1 + +Cancelling an order +-------------------- + +Now we will show how to cancel an order if the maker no +long wishes to exchange his WETH tokens. We will use a second example +order to demonstrate. + +>>> example_order_2 = { +... 'makerAddress': '0x5409ed021d9299bf6814279a6a1411a7e866a631', +... 'takerAddress': '0x0000000000000000000000000000000000000000', +... 'exchangeAddress': '0x4f833a24e1f95d70f028921e27040ca56e09ab0b', +... 'senderAddress': '0x0000000000000000000000000000000000000000', +... 'feeRecipientAddress': '0x0000000000000000000000000000000000000000', +... 'makerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'takerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'e41d2489571d322189246dafa5ebde1f4699f498'), +... 'salt': random.randint(1, 100000000000000000), +... 'makerFee': 0, +... 'takerFee': 0, +... 'makerAssetAmount': 1000000000000000000, +... 'takerAssetAmount': 500000000000000000000, +... 'expirationTimeSeconds': 999999999999999999999} +>>> tx_hash = zero_ex_exchange.cancel_order( +... order=example_order_2, tx_params=TxParams(from_=maker)) + +Once the transaction is mined, we can get the details of +our cancellation through the exchange wrapper. + +>>> cancel_event = zero_ex_exchange.get_cancel_event(tx_hash); +>>> cancelled_order_hash = cancel_event[0].args.orderHash.hex() + +Batching orders +---------------- + +The 0x exchange contract can also process multiple orders at +the same time. Here is an example where the taker fills +two orders in one transaction. + +>>> order_1 = { +... 'makerAddress': '0x5409ed021d9299bf6814279a6a1411a7e866a631', +... 'takerAddress': '0x0000000000000000000000000000000000000000', +... 'senderAddress': '0x0000000000000000000000000000000000000000', +... 'feeRecipientAddress': '0x0000000000000000000000000000000000000000', +... 'makerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'takerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'salt': random.randint(1, 100000000000000000), +... 'makerFee': 0, +... 'takerFee': 0, +... 'makerAssetAmount': 100, +... 'takerAssetAmount': 100, +... 'expirationTimeSeconds': 1000000000000000000} +>>> order_hash_1 = generate_order_hash_hex( +... order_1, zero_ex_exchange.address) +>>> signature_1 = sign_hash( +... provider, to_checksum_address(maker), order_hash_1) +>>> order_2 = { +... 'makerAddress': '0x5409ed021d9299bf6814279a6a1411a7e866a631', +... 'takerAddress': '0x0000000000000000000000000000000000000000', +... 'senderAddress': '0x0000000000000000000000000000000000000000', +... 'feeRecipientAddress': '0x0000000000000000000000000000000000000000', +... 'makerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'takerAssetData': bytes.fromhex( +... 'f47261b0000000000000000000000000' +... 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'), +... 'salt': random.randint(1, 100000000000000000), +... 'makerFee': 0, +... 'takerFee': 0, +... 'makerAssetAmount': 200, +... 'takerAssetAmount': 200, +... 'expirationTimeSeconds': 2000000000000000000} +>>> order_hash_2 = generate_order_hash_hex( +... order_2, zero_ex_exchange.address) +>>> signature_2 = sign_hash( +... provider, to_checksum_address(maker), order_hash_2) + +Fill order_1 and order_2 together: + +>>> tx_hash = zero_ex_exchange.batch_fill_orders( +... orders=[order_1, order_2], +... taker_amounts=[1, 2], +... signatures=[signature_1, signature_2], +... tx_params=TxParams(from_=taker)) +""" + +from .tx_params import TxParams +from .erc20_wrapper import ERC20Token +from .exchange_wrapper import Exchange diff --git a/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/_base_contract_wrapper.py b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/_base_contract_wrapper.py new file mode 100644 index 0000000000..b5986d630b --- /dev/null +++ b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/_base_contract_wrapper.py @@ -0,0 +1,129 @@ +"""Base wrapper class for accessing ethereum smart contracts.""" + +from typing import Optional, Union + +from eth_utils import to_checksum_address +from web3 import Web3 +from web3.providers.base import BaseProvider + +from .tx_params import TxParams + + +class BaseContractWrapper: + """Base class for wrapping ethereum smart contracts. + + It provides functionality for instantiating a contract instance, + calling view functions, and calling functions which require + transactions. + + :param provider: instance of :class:`web3.providers.base.BaseProvider` + :param account_address: default None, str of account address + :param private_key: default None, str of private_key + """ + + def __init__( + self, + provider: BaseProvider, + account_address: str = None, + private_key: str = None, + ): + """Create an instance of BaseContractWrapper.""" + self._provider = provider + self._account_address = account_address + self._private_key = private_key + self._web3 = Web3(provider) + self._web3_eth = self._web3.eth # pylint: disable=no-member + + self._can_send_tx = False + if self._web3_eth.defaultAccount or self._web3_eth.accounts: + self._can_send_tx = True + else: + middleware_stack = getattr(self._web3, "middleware_stack") + if middleware_stack.get("sign_and_send_raw_middleware"): + self._can_send_tx = True + elif private_key: + self._private_key = private_key + self._web3_eth.defaultAccount = to_checksum_address( + self._web3_eth.account.privateKeyToAccount( + private_key + ).address + ) + self._can_send_tx = True + + def _contract_instance(self, address: str, abi: dict): + """Get a contract instance. + + :param address: string address of contract + :param abi: dict contract ABI + + :returns: instance of contract + """ + return self._web3_eth.contract( + address=to_checksum_address(address), abi=abi + ) + + def _validate_and_checksum_address(self, address: str): + if not self._web3.isAddress(address): + raise TypeError("Invalid address provided: {}".format(address)) + return to_checksum_address(address) + + def _invoke_function_call(self, func, tx_params, view_only): + if view_only: + return func.call() + if not self._can_send_tx: + raise Exception( + "Cannot send transaction because no local private_key" + " or account found." + ) + if not tx_params: + tx_params = TxParams() + if not tx_params.from_: + tx_params.from_ = ( + self._web3_eth.defaultAccount or self._web3_eth.accounts[0] + ) + tx_params.from_ = self._validate_and_checksum_address(tx_params.from_) + if self._private_key: + res = self._sign_and_send_raw_direct(func, tx_params) + else: + res = func.transact(tx_params.as_dict()) + return res + + def _sign_and_send_raw_direct(self, func, tx_params): + transaction = func.buildTransaction(tx_params.as_dict()) + signed_tx = self._web3_eth.account.signTransaction( + transaction, private_key=self._private_key + ) + return self._web3_eth.sendRawTransaction(signed_tx.rawTransaction) + + def execute_method( + self, + address: str, + abi: dict, + method: str, + args: Optional[Union[list, tuple]] = None, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> str: + """Execute the method on a contract instance. + + :param address: string of contract address + :param abi: dict of contract ABI + :param method: string name of method to call + :param args: default None, list or tuple of arguments for the method + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether the transaction + should only be validated. + + :returns: str of transaction hash + """ + contract_instance = self._contract_instance(address=address, abi=abi) + if args is None: + args = [] + if hasattr(contract_instance.functions, method): + func = getattr(contract_instance.functions, method)(*args) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + raise Exception( + "No method {} found on contract {}.".format(address, method) + ) diff --git a/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/erc20_wrapper.py b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/erc20_wrapper.py new file mode 100644 index 0000000000..9763e40390 --- /dev/null +++ b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/erc20_wrapper.py @@ -0,0 +1,224 @@ +"""Wrapper for Ethereum ERC20 Token smart contract.""" + +from typing import Optional, Tuple, Union + +from hexbytes import HexBytes +from web3.datastructures import AttributeDict +from web3.providers.base import BaseProvider + +from zero_ex.contract_artifacts import abi_by_name + +from ._base_contract_wrapper import BaseContractWrapper +from .tx_params import TxParams + + +class ERC20Token(BaseContractWrapper): + """Wrapper class for Ethereum ERC20 smart contract. + + :param provider: instance of :class:`web3.providers.base.BaseProvider` + :param account_address: default None, str of account address + :param private_key: default None, str of private_key + """ + + def __init__( + self, + provider: BaseProvider, + account_address: str = None, + private_key: str = None, + ): + """Get an instance of wrapper for ERC20 smart contract.""" + super(ERC20Token, self).__init__( + provider=provider, + account_address=account_address, + private_key=private_key, + ) + + def _erc20(self, token_address): + """Get an instance of the ERC20 smart contract at a specific address. + + :param token_address: string address of token smart contract + + :returns: ERC20 contract object + """ + return self._contract_instance( + address=token_address, abi=abi_by_name("ERC20Token") + ) + + def transfer( + self, + token_address: str, + to_address: str, + value: int, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Transfer the balance from owner's account to another account. + + :param token_address: string address of token smart contract + :param to_address: string address of receiver + :param value: integer amount to send in Wei base unit + :param tx_params: default None, dict of transaction options + :param view_only: default False, boolean of whether to transact or + view only + + :returns: transaction hash + """ + token_address = self._validate_and_checksum_address(token_address) + to_address = self._validate_and_checksum_address(to_address) + # safeguard against fractional inputs + value = int(value) + func = self._erc20(token_address).functions.transfer(to_address, value) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def approve( + self, + token_address: str, + spender_address: str, + value: int, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Approve a `spender_address` to spend up to `value` your account. + + :param token_address: string address of token smart contract + :param spender_address: string address of receiver + :param value: integer amount of allowance in Wei base unit + :param tx_params: default None, dict of transaction options + :param view_only: default False, boolean of whether to transact or + view only + + :returns: transaction hash + """ + token_address = self._validate_and_checksum_address(token_address) + spender_address = self._validate_and_checksum_address(spender_address) + # safeguard against fractional inputs + value = int(value) + func = self._erc20(token_address).functions.approve( + spender_address, value + ) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def transfer_from( + self, + token_address: str, + authorized_address: str, + to_address: str, + value: int, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Transfer tokens from `authorized_address` to another address. + + Note that the `authorized_address` must have called with `approve` + with your address as the `spender_address`. + + :param token_address: string address of token smart contract + :param authorized_address: string address you have been authorized to + to transfer tokens from + :param to_address: string address of receiver + :param value: integer amount to send in Wei base unit + :param tx_params: default None, dict of transaction options + :param view_only: default False, boolean of whether to transact or + view only + + :returns: transaction hash + """ + token_address = self._validate_and_checksum_address(token_address) + authorized_address = self._validate_and_checksum_address( + authorized_address + ) + to_address = self._validate_and_checksum_address(to_address) + # safeguard against fractional inputs + value = int(value) + func = self._erc20(token_address).functions.transferFrom( + authorized_address, to_address, value + ) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def total_supply(self, token_address: str) -> int: + """Get total supply of a given ERC20 Token. + + :param token_address: string address of token smart contract + + :returns: integer amount of tokens in Wei + """ + token_address = self._validate_and_checksum_address(token_address) + func = self._erc20(token_address).functions.totalSupply() + return self._invoke_function_call( + func=func, tx_params=None, view_only=True + ) + + def balance_of(self, token_address: str, owner_address: str) -> int: + """Get token balance of a given owner address. + + :param token_address: string address of token smart contract + :param owner_address: string address of owner to check balance for + + :returns: integer amount of tokens in Wei the owner has + """ + token_address = self._validate_and_checksum_address(token_address) + owner_address = self._validate_and_checksum_address(owner_address) + func = self._erc20(token_address).functions.balanceOf(owner_address) + return self._invoke_function_call( + func=func, tx_params=None, view_only=True + ) + + def allowance( + self, token_address: str, owner_address: str, spender_address: str + ) -> Union[HexBytes, bytes]: + """Get the amount of tokens approved for a spender. + + :param token_address: string address of token smart contract + :param owner_address: string address of owner of the tokens + :param spender_address: string address of spender to be checked + + :returns: integer amount of tokens in Wei spender is authorized + to spend + """ + token_address = self._validate_and_checksum_address(token_address) + owner_address = self._validate_and_checksum_address(owner_address) + spender_address = self._validate_and_checksum_address(spender_address) + func = self._erc20(token_address).functions.allowance( + owner_address, spender_address + ) + return self._invoke_function_call( + func=func, tx_params=None, view_only=True + ) + + def get_transfer_event( + self, token_address: str, tx_hash: Union[HexBytes, bytes] + ) -> Tuple[AttributeDict]: + """Get the result of a transfer from its transaction hash. + + :param token_address: string address of token smart contract + :param tx_hash: `HexBytes` hash of transfer transaction + """ + tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash) + token_address = self._validate_and_checksum_address(token_address) + return ( + self._erc20(token_address) + .events.Transfer() + .processReceipt(tx_receipt) + ) + + def get_approval_event( + self, token_address: str, tx_hash: Union[HexBytes, bytes] + ) -> Tuple[AttributeDict]: + """Get the result of an approval event from its transaction hash. + + :param token_address: string address of token smart contract + :param tx_hash: `HexBytes` hash of approval transaction + """ + tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash) + token_address = self._validate_and_checksum_address(token_address) + return ( + self._erc20(token_address) + .events.Approval() + .processReceipt(tx_receipt) + ) diff --git a/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/exchange_wrapper.py b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/exchange_wrapper.py new file mode 100644 index 0000000000..fbdf8d63cd --- /dev/null +++ b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/exchange_wrapper.py @@ -0,0 +1,324 @@ +"""Wrapper for 0x Exchange smart contract.""" + +from typing import List, Optional, Tuple, Union +from itertools import repeat + +from eth_utils import remove_0x_prefix +from hexbytes import HexBytes +from web3.providers.base import BaseProvider +from web3.datastructures import AttributeDict + +from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId +from zero_ex.contract_artifacts import abi_by_name +from zero_ex.json_schemas import assert_valid +from zero_ex.order_utils import ( + Order, + generate_order_hash_hex, + is_valid_signature, + order_to_jsdict, +) + +from ._base_contract_wrapper import BaseContractWrapper +from .tx_params import TxParams + + +class CancelDisallowedError(Exception): + """Exception for when Cancel is not allowed.""" + + +class Exchange(BaseContractWrapper): + """Wrapper class for 0x Exchange smart contract. + + :param provider: instance of :class:`web3.providers.base.BaseProvider` + :param account_address: default None, str of account address + :param private_key: default None, str of private_key + """ + + def __init__( + self, + provider: BaseProvider, + account_address: str = None, + private_key: str = None, + ): + """Get an instance of the 0x Exchange smart contract wrapper.""" + super(Exchange, self).__init__( + provider=provider, + account_address=account_address, + private_key=private_key, + ) + self._web3_net = self._web3.net # pylint: disable=no-member + self.address = NETWORK_TO_ADDRESSES[ + NetworkId(int(self._web3_net.version)) + ].exchange + self._exchange = self._contract_instance( + address=self.address, abi=abi_by_name("Exchange") + ) + + def fill_order( + self, + order: Order, + taker_amount: int, + signature: str, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Fill a signed order with given amount of taker asset. + + This is the most basic way to fill an order. All of the other methods + call fillOrder under the hood with additional logic. This function + will attempt to fill the amount specified by the caller. However, if + the remaining fillable amount is less than the amount specified, the + remaining amount will be filled. Partial fills are allowed when + filling orders. + + See the specification docs for `fillOrder + `_. + + :param order: instance of :class:`zero_ex.order_utils.Order` + :param taker_amount: integer taker amount in Wei (1 Wei is 10e-18 ETH) + :param signature: str or hexstr or bytes of order hash signature + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether to transact or + view only + + :returns: `HexBytes` transaction hash + """ + assert_valid(order_to_jsdict(order, self.address), "/orderSchema") + is_valid_signature( + self._provider, + generate_order_hash_hex(order, self.address), + signature, + order["makerAddress"], + ) + # safeguard against fractional inputs + taker_fill_amount = int(taker_amount) + normalized_signature = bytes.fromhex(remove_0x_prefix(signature)) + func = self._exchange.functions.fillOrder( + order, taker_fill_amount, normalized_signature + ) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def batch_fill_orders( + self, + orders: List[Order], + taker_amounts: List[int], + signatures: List[str], + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Call `fillOrder` sequentially for orders, amounts and signatures. + + :param orders: list of instances of :class:`zero_ex.order_utils.Order` + :param taker_amounts: list of integer taker amounts in Wei + :param signatures: list of str|hexstr|bytes of order hash signature + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether to transact or + view only + + :returns: `HexBytes` transaction hash + """ + order_jsdicts = [ + order_to_jsdict(order, self.address) for order in orders + ] + map(assert_valid, order_jsdicts, repeat("/orderSchema")) + # safeguard against fractional inputs + normalized_fill_amounts = [ + int(taker_fill_amount) for taker_fill_amount in taker_amounts + ] + normalized_signatures = [ + bytes.fromhex(remove_0x_prefix(signature)) + for signature in signatures + ] + func = self._exchange.functions.batchFillOrders( + orders, normalized_fill_amounts, normalized_signatures + ) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def fill_or_kill_order( + self, + order: Order, + taker_amount: int, + signature: str, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Attemp to `fillOrder`, revert if fill is not exact amount. + + :param order: instance of :class:`zero_ex.order_utils.Order` + :param taker_amount: integer taker amount in Wei (1 Wei is 10e-18 ETH) + :param signature: str or hexstr or bytes of order hash signature + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether to transact or + view only + + :returns: `HexBytes` transaction hash + """ + assert_valid(order_to_jsdict(order, self.address), "/orderSchema") + is_valid_signature( + self._provider, + generate_order_hash_hex(order, self.address), + signature, + order["makerAddress"], + ) + # safeguard against fractional inputs + taker_fill_amount = int(taker_amount) + normalized_signature = bytes.fromhex(remove_0x_prefix(signature)) + func = self._exchange.functions.fillOrKillOrder( + order, taker_fill_amount, normalized_signature + ) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def batch_fill_or_kill_orders( + self, + orders: List[Order], + taker_amounts: List[int], + signatures: List[str], + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Call `fillOrKillOrder` sequentially for orders. + + :param orders: list of instances of :class:`zero_ex.order_utils.Order` + :param taker_amounts: list of integer taker amounts in Wei + :param signatures: list of str|hexstr|bytes of order hash signature + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether to transact or + view only + + :returns: `HexBytes` transaction hash + """ + order_jsdicts = [ + order_to_jsdict(order, self.address) for order in orders + ] + map(assert_valid, order_jsdicts, repeat("/orderSchema")) + # safeguard against fractional inputs + normalized_fill_amounts = [ + int(taker_fill_amount) for taker_fill_amount in taker_amounts + ] + normalized_signatures = [ + bytes.fromhex(remove_0x_prefix(signature)) + for signature in signatures + ] + func = self._exchange.functions.batchFillOrKillOrders( + orders, normalized_fill_amounts, normalized_signatures + ) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def cancel_order( + self, + order: Order, + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Cancel an order. + + See the specification docs for `cancelOrder + `_. + + :param order: instance of :class:`zero_ex.order_utils.Order` + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether to transact or + view only + + :returns: `HexBytes` transaction hash + """ + assert_valid(order_to_jsdict(order, self.address), "/orderSchema") + maker_address = self._validate_and_checksum_address( + order["makerAddress"] + ) + + if tx_params and tx_params.from_: + self._raise_if_maker_not_canceller( + maker_address, + self._validate_and_checksum_address(tx_params.from_), + ) + elif self._web3_eth.defaultAccount: + self._raise_if_maker_not_canceller( + maker_address, self._web3_eth.defaultAccount + ) + func = self._exchange.functions.cancelOrder(order) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def batch_cancel_orders( + self, + orders: List[Order], + tx_params: Optional[TxParams] = None, + view_only: bool = False, + ) -> Union[HexBytes, bytes]: + """Call `cancelOrder` sequentially for provided orders. + + :param orders: list of instance of :class:`zero_ex.order_utils.Order` + :param tx_params: default None, :class:`TxParams` transaction params + :param view_only: default False, boolean of whether to transact or + view only + + :returns: `HexBytes` transaction hash + """ + order_jsdicts = [ + order_to_jsdict(order, self.address) for order in orders + ] + map(assert_valid, order_jsdicts, repeat("/orderSchema")) + maker_addresses = [ + self._validate_and_checksum_address(order["makerAddress"]) + for order in orders + ] + if tx_params and tx_params.from_: + map( + self._raise_if_maker_not_canceller, + maker_addresses, + repeat(tx_params.from_), + ) + elif self._web3_eth.defaultAccount: + map( + self._raise_if_maker_not_canceller, + maker_addresses, + repeat(self._web3_eth.defaultAccount), + ) + func = self._exchange.functions.batchCancelOrders(orders) + return self._invoke_function_call( + func=func, tx_params=tx_params, view_only=view_only + ) + + def get_fill_event( + self, tx_hash: Union[HexBytes, bytes] + ) -> Tuple[AttributeDict]: + """Get fill event for a fill transaction. + + :param tx_hash: `HexBytes` hash of fill transaction + + :returns: tuple of `FillResults`. + """ + tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash) + return self._exchange.events.Fill().processReceipt(tx_receipt) + + def get_cancel_event( + self, tx_hash: Union[HexBytes, bytes] + ) -> Tuple[AttributeDict]: + """Get cancel event for cancel transaction. + + :param tx_hash: `HexBytes` hash of cancel transaction + """ + tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash) + return self._exchange.events.Cancel().processReceipt(tx_receipt) + + @staticmethod + def _raise_if_maker_not_canceller(maker_address, canceller_address): + """Raise exception is maker is not same as canceller.""" + if maker_address != canceller_address: + raise CancelDisallowedError( + "Order with makerAddress {} can not be cancelled by {}".format( + maker_address, canceller_address + ) + ) diff --git a/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/py.typed b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/tx_params.py b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/tx_params.py new file mode 100644 index 0000000000..4b864898ec --- /dev/null +++ b/python-packages/contract_wrappers/src/zero_ex/contract_wrappers/tx_params.py @@ -0,0 +1,39 @@ +"""Transaction parameters for use with contract wrappers.""" + +from typing import Optional + +import attr + + +@attr.s(kw_only=True) +class TxParams: + """Transaction parameters for use with contract wrappers. + + :param from_: default None, string of account address to initiate tx from + :param value: default None, integer of amount of ETH in Wei for transfer + :param gas: default None, integer maximum amount of ETH in Wei for gas + :param grasPrice: default None, integer price of unit of gas + :param nonce: default None, integer nonce for account + """ + + from_: Optional[str] = attr.ib(default=None) + value: Optional[int] = attr.ib( + default=None, converter=attr.converters.optional(int) + ) + gas: Optional[int] = attr.ib( + default=None, converter=attr.converters.optional(int) + ) + gasPrice: Optional[int] = attr.ib( + default=None, converter=attr.converters.optional(int) + ) + nonce: Optional[int] = attr.ib( + default=None, converter=attr.converters.optional(int) + ) + + def as_dict(self): + """Get transaction params as dict appropriate for web3.""" + res = {k: v for k, v in attr.asdict(self).items() if v is not None} + if "from_" in res: + res["from"] = res["from_"] + del res["from_"] + return res diff --git a/python-packages/contract_wrappers/stubs/distutils/__init__.pyi b/python-packages/contract_wrappers/stubs/distutils/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/stubs/distutils/command/__init__.pyi b/python-packages/contract_wrappers/stubs/distutils/command/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/stubs/distutils/command/clean.pyi b/python-packages/contract_wrappers/stubs/distutils/command/clean.pyi new file mode 100644 index 0000000000..46a42ddb13 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/distutils/command/clean.pyi @@ -0,0 +1,7 @@ +from distutils.core import Command + +class clean(Command): + def initialize_options(self: clean) -> None: ... + def finalize_options(self: clean) -> None: ... + def run(self: clean) -> None: ... + ... diff --git a/python-packages/contract_wrappers/stubs/eth_account/__init__.pyi b/python-packages/contract_wrappers/stubs/eth_account/__init__.pyi new file mode 100644 index 0000000000..5caed9e96f --- /dev/null +++ b/python-packages/contract_wrappers/stubs/eth_account/__init__.pyi @@ -0,0 +1 @@ +class Account: ... diff --git a/python-packages/contract_wrappers/stubs/eth_account/local.pyi b/python-packages/contract_wrappers/stubs/eth_account/local.pyi new file mode 100644 index 0000000000..d0b3515699 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/eth_account/local.pyi @@ -0,0 +1,3 @@ +class LocalAccount: + address: str + ... diff --git a/python-packages/contract_wrappers/stubs/eth_utils/__init__.pyi b/python-packages/contract_wrappers/stubs/eth_utils/__init__.pyi new file mode 100644 index 0000000000..90e1277eb0 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/eth_utils/__init__.pyi @@ -0,0 +1,3 @@ +def to_checksum_address(address: str) -> str: ... + +def remove_0x_prefix(hex_string: str) -> str: ... \ No newline at end of file diff --git a/python-packages/contract_wrappers/stubs/hexbytes/__init__.pyi b/python-packages/contract_wrappers/stubs/hexbytes/__init__.pyi new file mode 100644 index 0000000000..bc88efe52a --- /dev/null +++ b/python-packages/contract_wrappers/stubs/hexbytes/__init__.pyi @@ -0,0 +1 @@ +class HexBytes: ... diff --git a/python-packages/contract_wrappers/stubs/pytest/__init__.pyi b/python-packages/contract_wrappers/stubs/pytest/__init__.pyi new file mode 100644 index 0000000000..d9209ab5b9 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/pytest/__init__.pyi @@ -0,0 +1,9 @@ +from typing import Callable + +def fixture(scope: str) -> Callable: + ... + +class ExceptionInfo: + ... + +def raises(exception: Exception) -> ExceptionInfo: ... \ No newline at end of file diff --git a/python-packages/contract_wrappers/stubs/setuptools/__init__.pyi b/python-packages/contract_wrappers/stubs/setuptools/__init__.pyi new file mode 100644 index 0000000000..8ea8d32b7e --- /dev/null +++ b/python-packages/contract_wrappers/stubs/setuptools/__init__.pyi @@ -0,0 +1,8 @@ +from distutils.dist import Distribution +from typing import Any, List + +def setup(**attrs: Any) -> Distribution: ... + +class Command: ... + +def find_packages(where: str) -> List[str]: ... diff --git a/python-packages/contract_wrappers/stubs/setuptools/command/__init__.pyi b/python-packages/contract_wrappers/stubs/setuptools/command/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/stubs/setuptools/command/test.pyi b/python-packages/contract_wrappers/stubs/setuptools/command/test.pyi new file mode 100644 index 0000000000..c5ec770ad3 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/setuptools/command/test.pyi @@ -0,0 +1,3 @@ +from setuptools import Command + +class test(Command): ... diff --git a/python-packages/contract_wrappers/stubs/sha3/__init__.pyi b/python-packages/contract_wrappers/stubs/sha3/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/stubs/web3/__init__.pyi b/python-packages/contract_wrappers/stubs/web3/__init__.pyi new file mode 100644 index 0000000000..82e638b479 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/web3/__init__.pyi @@ -0,0 +1,56 @@ +from typing import Any, Callable, Dict, List, Optional, Union + +from hexbytes import HexBytes +from eth_account.local import LocalAccount +from web3 import datastructures +from web3.utils import datatypes +from web3.providers.base import BaseProvider + + +class Web3: + class HTTPProvider(BaseProvider): + ... + + def __init__(self, provider: BaseProvider) -> None: ... + + @staticmethod + def sha3( + primitive: Optional[Union[bytes, int, None]] = None, + text: Optional[str] = None, + hexstr: Optional[str] = None + ) -> bytes: ... + + @staticmethod + def isAddress(address: str) -> bool: ... + + class middleware_stack: + @staticmethod + def get(key: str) -> Callable: ... + ... + + class net: + version: str + ... + + + class eth: + defaultAccount: str + accounts: List[str] + ... + + class account: + @staticmethod + def privateKeyToAccount(private_key: str) -> LocalAccount: ... + ... + + @staticmethod + def getTransactionReceipt(tx_hash: Union[HexBytes, bytes]) -> Any: ... + + @staticmethod + def contract(address: str, abi: Dict) -> datatypes.Contract: ... + ... + + @staticmethod + def isAddress(address: str) -> bool: ... + ... + ... diff --git a/python-packages/contract_wrappers/stubs/web3/datastructures.pyi b/python-packages/contract_wrappers/stubs/web3/datastructures.pyi new file mode 100644 index 0000000000..fac8f29268 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/web3/datastructures.pyi @@ -0,0 +1,5 @@ +class NamedElementOnion: + ... + +class AttributeDict: + ... \ No newline at end of file diff --git a/python-packages/contract_wrappers/stubs/web3/exceptions.pyi b/python-packages/contract_wrappers/stubs/web3/exceptions.pyi new file mode 100644 index 0000000000..83abf973d1 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/web3/exceptions.pyi @@ -0,0 +1,2 @@ +class BadFunctionCallOutput(Exception): + ... diff --git a/python-packages/contract_wrappers/stubs/web3/providers/__init__.pyi b/python-packages/contract_wrappers/stubs/web3/providers/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/stubs/web3/providers/base.pyi b/python-packages/contract_wrappers/stubs/web3/providers/base.pyi new file mode 100644 index 0000000000..82ca9e3da1 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/web3/providers/base.pyi @@ -0,0 +1,2 @@ +class BaseProvider: + ... diff --git a/python-packages/contract_wrappers/stubs/web3/utils/__init__.pyi b/python-packages/contract_wrappers/stubs/web3/utils/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-packages/contract_wrappers/stubs/web3/utils/datatypes.pyi b/python-packages/contract_wrappers/stubs/web3/utils/datatypes.pyi new file mode 100644 index 0000000000..70baff3728 --- /dev/null +++ b/python-packages/contract_wrappers/stubs/web3/utils/datatypes.pyi @@ -0,0 +1,3 @@ +class Contract: + def call(self): ... + ... diff --git a/python-packages/contract_wrappers/test/__init__.py b/python-packages/contract_wrappers/test/__init__.py new file mode 100644 index 0000000000..30ad154b25 --- /dev/null +++ b/python-packages/contract_wrappers/test/__init__.py @@ -0,0 +1 @@ +"""Tests of zero_ex.contract_wrappers.""" diff --git a/python-packages/contract_wrappers/test/conftest.py b/python-packages/contract_wrappers/test/conftest.py new file mode 100644 index 0000000000..789ca639ed --- /dev/null +++ b/python-packages/contract_wrappers/test/conftest.py @@ -0,0 +1,79 @@ +"""Fixtures for pytest.""" + +import pytest +from eth_utils import remove_0x_prefix, to_checksum_address +from web3 import Web3 + +from zero_ex.order_utils import asset_data_utils +from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId +from zero_ex.contract_artifacts import abi_by_name + + +@pytest.fixture(scope="module") +def ganache_provider(): + """Get a ganache web3 provider.""" + return Web3.HTTPProvider(endpoint_uri="http://127.0.0.1:8545") + + +@pytest.fixture(scope="module") +def web3_instance(ganache_provider): # pylint: disable=redefined-outer-name + """Get a web3 instance.""" + return Web3(ganache_provider) + + +@pytest.fixture(scope="module") +def web3_eth(web3_instance): # pylint: disable=redefined-outer-name + """Get web3 instance's eth member.""" + return web3_instance.eth # pylint: disable=no-member + + +@pytest.fixture(scope="module") +def accounts(web3_eth): # pylint: disable=redefined-outer-name + """Get the accounts associated with the test web3_eth instance.""" + return web3_eth.accounts + + +@pytest.fixture(scope="module") +def erc20_proxy_address(): + """Get the 0x ERC20 Proxy address.""" + return NETWORK_TO_ADDRESSES[NetworkId.GANACHE].erc20_proxy + + +@pytest.fixture(scope="module") +def weth_address(): + """Get address of Wrapped Ether (WETH) token for the Ganache network.""" + return NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token + + +@pytest.fixture(scope="module") +def weth_asset_data(weth_address): # pylint: disable=redefined-outer-name + """Get 0x asset data for Wrapped Ether (WETH) token.""" + return bytes.fromhex( + remove_0x_prefix( + asset_data_utils.encode_erc20_asset_data(weth_address) + ) + ) + + +@pytest.fixture(scope="module") +def weth_instance( + web3_eth, weth_address +): # pylint: disable=redefined-outer-name + """Get an instance of the WrapperEther contract.""" + return web3_eth.contract( + address=to_checksum_address(weth_address), abi=abi_by_name("WETH9") + ) + + +@pytest.fixture(scope="module") +def zrx_address(): + """Get address of ZRX token for Ganache network.""" + return NETWORK_TO_ADDRESSES[NetworkId.GANACHE].zrx_token + + +@pytest.fixture(scope="module") +def zrx_asset_data(zrx_address): # pylint: disable=redefined-outer-name + """Get 0x asset data for ZRX token.""" + return bytes.fromhex( + remove_0x_prefix(asset_data_utils.encode_erc20_asset_data(zrx_address)) + ) diff --git a/python-packages/contract_wrappers/test/test_base_contract_wrapper.py b/python-packages/contract_wrappers/test/test_base_contract_wrapper.py new file mode 100644 index 0000000000..8b5ca34dc5 --- /dev/null +++ b/python-packages/contract_wrappers/test/test_base_contract_wrapper.py @@ -0,0 +1,47 @@ +"""Tests for :class:`BaseContractWrapper`.""" + +import pytest +from eth_utils import to_checksum_address + +from zero_ex.contract_artifacts import abi_by_name +from zero_ex.contract_wrappers._base_contract_wrapper import ( + BaseContractWrapper, +) + + +@pytest.fixture(scope="module") +def contract_wrapper(ganache_provider): + """Get a BaseContractWrapper instance for testing.""" + return BaseContractWrapper(provider=ganache_provider) + + +def test_contract_wrapper__execute_method( + accounts, + contract_wrapper, # pylint: disable=redefined-outer-name + erc20_proxy_address, + weth_address, # pylint: disable=redefined-outer-name +): + """Test :function:`BaseContractWrapper.execute` method.""" + acc1_allowance = contract_wrapper.execute_method( + address=weth_address, + abi=abi_by_name("WETH9"), + method="allowance", + view_only=True, + args=( + to_checksum_address(accounts[3]), + to_checksum_address(erc20_proxy_address), + ), + ) + assert acc1_allowance == 0 + + with pytest.raises(Exception): + contract_wrapper.execute_method( + address=weth_address, + abi=abi_by_name("WETH9"), + method="send", + view_only=True, + args=[ + to_checksum_address(accounts[3]), + to_checksum_address(erc20_proxy_address), + ], + ) diff --git a/python-packages/contract_wrappers/test/test_erc20_wrapper.py b/python-packages/contract_wrappers/test/test_erc20_wrapper.py new file mode 100644 index 0000000000..ca3b1fbfc1 --- /dev/null +++ b/python-packages/contract_wrappers/test/test_erc20_wrapper.py @@ -0,0 +1,80 @@ +"""Tests for ERC20Token wrapper.""" + +from decimal import Decimal + +import pytest + +from zero_ex.contract_wrappers import ERC20Token, TxParams + + +MAX_ALLOWANCE = int("{:.0f}".format(Decimal(2) ** 256 - 1)) + + +@pytest.fixture(scope="module") +def erc20_wrapper(ganache_provider): + """Get an instance of ERC20Token wrapper class for testing.""" + return ERC20Token(ganache_provider) + + +def test_erc20_wrapper__balance_of( + accounts, + erc20_wrapper, # pylint: disable=redefined-outer-name + weth_address, + weth_instance, # pylint: disable=redefined-outer-name +): + """Test getting baance of an account for an ERC20 token.""" + acc1_original_weth_balance = erc20_wrapper.balance_of( + weth_address, accounts[0] + ) + acc2_original_weth_balance = erc20_wrapper.balance_of( + weth_address, accounts[1] + ) + + expected_difference = 1 * 10 ** 18 + + weth_instance.functions.deposit().transact( + {"from": accounts[0], "value": expected_difference} + ) + weth_instance.functions.deposit().transact( + {"from": accounts[1], "value": expected_difference} + ) + acc1_weth_balance = erc20_wrapper.balance_of(weth_address, accounts[0]) + acc2_weth_balance = erc20_wrapper.balance_of(weth_address, accounts[1]) + + assert ( + acc1_weth_balance - acc1_original_weth_balance == expected_difference + ) + assert ( + acc2_weth_balance - acc2_original_weth_balance == expected_difference + ) + + +def test_erc20_wrapper__approve( + accounts, + erc20_proxy_address, + erc20_wrapper, # pylint: disable=redefined-outer-name + weth_address, # pylint: disable=redefined-outer-name +): + """Test approving one account to spend balance from another account.""" + erc20_wrapper.approve( + weth_address, + erc20_proxy_address, + MAX_ALLOWANCE, + tx_params=TxParams(from_=accounts[0]), + ) + erc20_wrapper.approve( + weth_address, + erc20_proxy_address, + MAX_ALLOWANCE, + tx_params=TxParams(from_=accounts[1]), + ) + + acc_1_weth_allowance = erc20_wrapper.allowance( + weth_address, accounts[0], erc20_proxy_address + ) + acc_2_weth_allowance = erc20_wrapper.allowance( + weth_address, accounts[1], erc20_proxy_address + ) + + assert acc_1_weth_allowance == MAX_ALLOWANCE + assert acc_2_weth_allowance == MAX_ALLOWANCE diff --git a/python-packages/contract_wrappers/test/test_exchange_wrapper.py b/python-packages/contract_wrappers/test/test_exchange_wrapper.py new file mode 100644 index 0000000000..72f638e631 --- /dev/null +++ b/python-packages/contract_wrappers/test/test_exchange_wrapper.py @@ -0,0 +1,124 @@ +"""Test 0x Exchnage wrapper.""" + +import random + +import pytest +from eth_utils import remove_0x_prefix + +from zero_ex.contract_wrappers import Exchange, TxParams +from zero_ex.json_schemas import assert_valid +from zero_ex.order_utils import generate_order_hash_hex, Order, sign_hash + + +@pytest.fixture(scope="module") +def exchange_wrapper(ganache_provider): + """Get an Exchange wrapper instance.""" + return Exchange(provider=ganache_provider) + + +def create_test_order( + maker_address, + maker_asset_amount, + maker_asset_data, + taker_asset_amount, + taker_asset_data, +): + """Create a test order.""" + order: Order = { + "makerAddress": maker_address.lower(), + "takerAddress": "0x0000000000000000000000000000000000000000", + "feeRecipientAddress": "0x0000000000000000000000000000000000000000", + "senderAddress": "0x0000000000000000000000000000000000000000", + "makerAssetAmount": maker_asset_amount, + "takerAssetAmount": taker_asset_amount, + "makerFee": 0, + "takerFee": 0, + "expirationTimeSeconds": 100000000000000, + "salt": random.randint(1, 1000000000), + "makerAssetData": maker_asset_data, + "takerAssetData": taker_asset_data, + } + return order + + +def assert_fill_log(fill_log, maker, taker, order, order_hash): + """assert that the fill log matches the order details""" + assert fill_log.makerAddress == maker + assert fill_log.takerAddress == taker + assert fill_log.feeRecipientAddress == order["feeRecipientAddress"] + assert fill_log.senderAddress == taker + assert fill_log.orderHash == bytes.fromhex(remove_0x_prefix(order_hash)) + assert fill_log.makerAssetFilledAmount == order["makerAssetAmount"] + assert fill_log.takerAssetFilledAmount == order["takerAssetAmount"] + assert fill_log.makerFeePaid == order["makerFee"] + assert fill_log.takerFeePaid == order["takerFee"] + assert fill_log.makerAssetData == order["makerAssetData"] + assert fill_log.takerAssetData == order["takerAssetData"] + + +def test_exchange_wrapper__fill_order( + accounts, + exchange_wrapper, # pylint: disable=redefined-outer-name + ganache_provider, + weth_asset_data, +): + """Test filling an order.""" + taker = accounts[0] + maker = accounts[1] + exchange_address = exchange_wrapper.address + order = create_test_order(maker, 1, weth_asset_data, 1, weth_asset_data) + order_hash = generate_order_hash_hex( + order=order, exchange_address=exchange_address + ) + order_signature = sign_hash(ganache_provider, maker, order_hash) + + tx_hash = exchange_wrapper.fill_order( + order=order, + taker_amount=order["takerAssetAmount"], + signature=order_signature, + tx_params=TxParams(from_=taker), + ) + assert_valid(tx_hash.hex(), "/hexSchema") + + fill_event = exchange_wrapper.get_fill_event(tx_hash) + assert_fill_log(fill_event[0].args, maker, taker, order, order_hash) + + +# pylint: disable=too-many-locals +def test_exchange_wrapper__batch_fill_orders( + accounts, + exchange_wrapper, # pylint: disable=redefined-outer-name + ganache_provider, + weth_asset_data, +): + """Test filling a batch of orders.""" + taker = accounts[0] + maker = accounts[1] + exchange_address = exchange_wrapper.address + orders = [] + order_1 = create_test_order(maker, 1, weth_asset_data, 1, weth_asset_data) + order_2 = create_test_order(maker, 1, weth_asset_data, 1, weth_asset_data) + orders.append(order_1) + orders.append(order_2) + order_hashes = [ + generate_order_hash_hex(order=order, exchange_address=exchange_address) + for order in orders + ] + order_signatures = [ + sign_hash(ganache_provider, maker, order_hash) + for order_hash in order_hashes + ] + taker_amounts = [order["takerAssetAmount"] for order in orders] + tx_hash = exchange_wrapper.batch_fill_orders( + orders=orders, + taker_amounts=taker_amounts, + signatures=order_signatures, + tx_params=TxParams(from_=taker), + ) + assert_valid(tx_hash.hex(), "/hexSchema") + + fill_events = exchange_wrapper.get_fill_event(tx_hash) + for index, order in enumerate(orders): + assert_fill_log( + fill_events[index].args, maker, taker, order, order_hashes[index] + ) diff --git a/python-packages/contract_wrappers/tox.ini b/python-packages/contract_wrappers/tox.ini new file mode 100644 index 0000000000..cd847a5bb8 --- /dev/null +++ b/python-packages/contract_wrappers/tox.ini @@ -0,0 +1,25 @@ +# tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py37 + +[testenv] +commands = + pip install -e .[dev] + python setup.py test + +[testenv:run_tests_against_test_deployment] +commands = + # install dependencies from real PyPI + pip install 0x-contract-addresses 0x-contract-artifacts 0x-json-schemas 0x-order-utils 0x-web3 attrs eth_utils hypothesis>=3.31.2 mypy_extensions pytest + # install package-under-test from test PyPI + pip install --index-url https://test.pypi.org/legacy/ 0x-contract-wrappers + pytest test + +[testenv:run_tests_against_deployment] +commands = + pip install 0x-contract-wrappers + pytest test