Python contract wrappers (#1721)
This commit is contained in:
parent
e043735362
commit
a256494ec8
@ -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",
|
||||
|
13
python-packages/contract_wrappers/.discharge.json
Normal file
13
python-packages/contract_wrappers/.discharge.json
Normal 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
|
||||
}
|
4
python-packages/contract_wrappers/.pylintrc
Normal file
4
python-packages/contract_wrappers/.pylintrc
Normal 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
|
45
python-packages/contract_wrappers/README.md
Normal file
45
python-packages/contract_wrappers/README.md
Normal 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.
|
226
python-packages/contract_wrappers/setup.py
Executable file
226
python-packages/contract_wrappers/setup.py
Executable 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"),
|
||||
}
|
||||
},
|
||||
)
|
56
python-packages/contract_wrappers/src/conf.py
Normal file
56
python-packages/contract_wrappers/src/conf.py
Normal 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}
|
40
python-packages/contract_wrappers/src/index.rst
Normal file
40
python-packages/contract_wrappers/src/index.rst
Normal 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`
|
@ -0,0 +1,2 @@
|
||||
"""0x Python API."""
|
||||
__import__("pkg_resources").declare_namespace(__name__)
|
@ -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
|
@ -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)
|
||||
)
|
@ -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)
|
||||
)
|
@ -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
|
||||
)
|
||||
)
|
@ -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
|
@ -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: ...
|
||||
...
|
@ -0,0 +1 @@
|
||||
class Account: ...
|
@ -0,0 +1,3 @@
|
||||
class LocalAccount:
|
||||
address: str
|
||||
...
|
@ -0,0 +1,3 @@
|
||||
def to_checksum_address(address: str) -> str: ...
|
||||
|
||||
def remove_0x_prefix(hex_string: str) -> str: ...
|
@ -0,0 +1 @@
|
||||
class HexBytes: ...
|
@ -0,0 +1,9 @@
|
||||
from typing import Callable
|
||||
|
||||
def fixture(scope: str) -> Callable:
|
||||
...
|
||||
|
||||
class ExceptionInfo:
|
||||
...
|
||||
|
||||
def raises(exception: Exception) -> ExceptionInfo: ...
|
@ -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]: ...
|
@ -0,0 +1,3 @@
|
||||
from setuptools import Command
|
||||
|
||||
class test(Command): ...
|
56
python-packages/contract_wrappers/stubs/web3/__init__.pyi
Normal file
56
python-packages/contract_wrappers/stubs/web3/__init__.pyi
Normal 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: ...
|
||||
...
|
||||
...
|
@ -0,0 +1,5 @@
|
||||
class NamedElementOnion:
|
||||
...
|
||||
|
||||
class AttributeDict:
|
||||
...
|
@ -0,0 +1,2 @@
|
||||
class BadFunctionCallOutput(Exception):
|
||||
...
|
@ -0,0 +1,2 @@
|
||||
class BaseProvider:
|
||||
...
|
@ -0,0 +1,3 @@
|
||||
class Contract:
|
||||
def call(self): ...
|
||||
...
|
1
python-packages/contract_wrappers/test/__init__.py
Normal file
1
python-packages/contract_wrappers/test/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests of zero_ex.contract_wrappers."""
|
79
python-packages/contract_wrappers/test/conftest.py
Normal file
79
python-packages/contract_wrappers/test/conftest.py
Normal 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))
|
||||
)
|
@ -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),
|
||||
],
|
||||
)
|
80
python-packages/contract_wrappers/test/test_erc20_wrapper.py
Normal file
80
python-packages/contract_wrappers/test/test_erc20_wrapper.py
Normal 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
|
124
python-packages/contract_wrappers/test/test_exchange_wrapper.py
Normal file
124
python-packages/contract_wrappers/test/test_exchange_wrapper.py
Normal 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]
|
||||
)
|
25
python-packages/contract_wrappers/tox.ini
Normal file
25
python-packages/contract_wrappers/tox.ini
Normal 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
|
Loading…
x
Reference in New Issue
Block a user