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.
|
# independent first) in order for them to resolve properly.
|
||||||
"contract_addresses",
|
"contract_addresses",
|
||||||
"contract_artifacts",
|
"contract_artifacts",
|
||||||
|
"contract_wrappers",
|
||||||
"json_schemas",
|
"json_schemas",
|
||||||
"sra_client",
|
"sra_client",
|
||||||
"order_utils",
|
"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