Python contract wrappers (#1721)

This commit is contained in:
Michael Huang 2019-03-25 18:25:41 -05:00 committed by F. Eugene Aumson
parent e043735362
commit a256494ec8
41 changed files with 1914 additions and 0 deletions

View File

@ -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",

View File

@ -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
}

View File

@ -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

View File

@ -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.

View File

@ -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"),
}
},
)

View File

@ -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}

View File

@ -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`

View File

@ -0,0 +1,2 @@
"""0x Python API."""
__import__("pkg_resources").declare_namespace(__name__)

View File

@ -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 <https://web3py.readthedocs.io/en/stable/providers.htm>`__. 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 <https://tokenallowance.io/>`__).
In our demo the taker asset is WETH (or Wrapped ETH, you can read about WETH
`here <https://weth.io/>`__).,
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
<http://0x-demos-py.s3-website-us-east-1.amazonaws.com/>`__
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

View File

@ -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)
)

View File

@ -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)
)

View File

@ -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
<https://github.com/0xProject/0x-protocol-specification/blob/master
/v2/v2-specification.md#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
<https://github.com/0xProject/0x-protocol-specification/blob/master
/v2/v2-specification.md#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
)
)

View File

@ -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

View File

@ -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: ...
...

View File

@ -0,0 +1 @@
class Account: ...

View File

@ -0,0 +1,3 @@
class LocalAccount:
address: str
...

View File

@ -0,0 +1,3 @@
def to_checksum_address(address: str) -> str: ...
def remove_0x_prefix(hex_string: str) -> str: ...

View File

@ -0,0 +1 @@
class HexBytes: ...

View File

@ -0,0 +1,9 @@
from typing import Callable
def fixture(scope: str) -> Callable:
...
class ExceptionInfo:
...
def raises(exception: Exception) -> ExceptionInfo: ...

View File

@ -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]: ...

View File

@ -0,0 +1,3 @@
from setuptools import Command
class test(Command): ...

View File

@ -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: ...
...
...

View File

@ -0,0 +1,5 @@
class NamedElementOnion:
...
class AttributeDict:
...

View File

@ -0,0 +1,2 @@
class BadFunctionCallOutput(Exception):
...

View File

@ -0,0 +1,2 @@
class BaseProvider:
...

View File

@ -0,0 +1,3 @@
class Contract:
def call(self): ...
...

View File

@ -0,0 +1 @@
"""Tests of zero_ex.contract_wrappers."""

View File

@ -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))
)

View File

@ -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),
],
)

View File

@ -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

View File

@ -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]
)

View File

@ -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