Auto-gen Python Exchange wrapper (#1919)

* Rename existing wrapper, to match contract name

* base contract: make member var public

* json_schemas.py: stop storing copies of schemas!

* .gitignore generated erc20_token.py wrapper

* json schemas: allow uppercase digits in address

* existing exchange wrapper: re-order methods

to match method order in Solidity contract, to reduce noise in upcoming
diffs of newly generated code vs. old manually-written code.

* existing exchange wrapper: rename method params

To match contract method param names

* existing exchange wrapper: remove redundant member

* existing exchange wrapper: make signatures bytes

Not strings.

* abi-gen/test-cli: show context on diff failure

* abi-gen-templates/Py: fix broken event interface

Previous changes had removed the `token_address` parameter from all
generated methods, but this instance was missed because there weren't
tests/examples using events for the first contract for which wrappers
were generated (ERC20Token).

* abi-gen: remove unused method parameters

* abi-gen: convert Py method params to snake case

* abi-gen: rewrite Python tuple handling

* python-generated-wrappers: include Exchange

* abi-gen-templates/Py: easy linter fixes

* abi-gen-templates/Py: satisfy docstring linters

* abi-gen-templates/Py: normalize bytes before use

* contract_wrappers.py: replace Exchange w/generated

* contract_wrappers.py: rm manually written Exchange

* contract_wrappers.py/doctest: rename variables

* abi-gen: fix misspelling in docstring

Co-Authored-By: Fabio B <me@fabioberger.com>

* Py docs: error on warning, and test build in CI

* abi-gen: doc Py bytes params as requiring UTF-8

* abi-gen: git mv diff.sh test-cli/

* abi-gen: put Py wrapper in module folder, not file

This leaves space for user-defined additions to the same module, such as
for custom types, as shown herein.

* abi-gen: customizable param validation for Python

* contract_wrappers.py: JSON schema Order validation

* CircleCI Build Artifacts

For abi-gen command-line test output, for generated Python contract
wrappers as output by abi-gen, for generated Python contract wrappers as
reformatted and included in the Python package area, and for the "build"
output folder in each Python package, which includes the generated
documentation.

* CHANGELOG updates for all components

* abi-gen: grammar in comments

Co-Authored-By: Fabio B <me@fabioberger.com>

* abi-gen: CHANGELOG spelling correction

Co-Authored-By: Fabio B <me@fabioberger.com>

* order_utils.py: reverse (chronological) CHANGELOG

* abi-gen-templates: reset CHANGELOG patch version

* CHANGELOGs: use multiple entries where appropriate

* abi-gen: enable devdoc solc output in test-cli

* abi-gen-templates/Py: consolidate return type

* abi-gen/test-cli: non-pure fixture contract method

Added a method to the "dummy" test fixture contract that isn't pure.
All of the other prior method cases were pure.

* abi-gen/Py: fix const methods missing return type

* abi-gen/Py: fix wrong return types on some methods

Specifically, wrapper methods wrapping contract methods that modify
contract state and return no return value.  There was no test case for
this.  Now there is.

* contract_wrappers.py: rm generated code in `clean`

* Parallelize Py monorepo scripts (test, lint, etc)
This commit is contained in:
F. Eugene Aumson
2019-07-23 12:58:18 -04:00
committed by GitHub
parent 1e6e74878f
commit ead8099109
120 changed files with 3246 additions and 2117 deletions

3
python-packages/build_docs Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
./parallel ./setup.py build_sphinx

View File

@@ -186,6 +186,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -60,15 +60,6 @@ class LintCommand(distutils.command.build_py.build_py):
"mypy src setup.py".split(),
# security issue checker:
"bandit -r src ./setup.py".split(),
# HACK: ensure contract artifacts match the authoritative copies:
# this is a hack. ideally we would symlink to the authoritative
# copies, but a problem with setuptools is preventing it from
# following symlinks when gathering package_data. see
# https://github.com/pypa/setuptools/issues/415.
(
"diff src/zero_ex/contract_artifacts/artifacts"
+ " ../../packages/contract-artifacts/artifacts"
).split(),
# general linter:
"pylint src setup.py".split(),
# pylint takes relatively long to run, so it runs last, to enable
@@ -225,6 +216,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -1,6 +1,3 @@
[MESSAGES CONTROL]
disable=C0330,line-too-long,fixme,too-few-public-methods,too-many-ancestors
disable=C0330,line-too-long,fixme,too-few-public-methods,too-many-ancestors,duplicate-code
# C0330 is "bad hanging indent". we use indents per `black`.
[SIMILARITIES]
min-similarity-lines=12

View File

@@ -1,5 +1,9 @@
# Changelog
## 2.0.0 - TBD
- Completely new implementation of the Exchange wrapper, virtually all auto-generated from the Solidity contract. Breaking changes include method parameter name changes and accepting of signatures as bytes.
## 1.0.0 - 2019-04-30
- Initial release.

View File

@@ -4,7 +4,7 @@
import subprocess # nosec
from shutil import copy, rmtree
from os import environ, path
from os import environ, path, remove
from pathlib import Path
from sys import argv
from importlib.util import find_spec
@@ -34,17 +34,37 @@ class PreInstallCommand(distutils.command.build_py.build_py):
"packages",
"python-contract-wrappers",
"generated",
"erc20_token.py",
"erc20_token",
"__init__.py",
),
path.join(
pkgdir, "src", "zero_ex", "contract_wrappers", "erc20_token"
),
)
copy(
path.join(
pkgdir,
"..",
"..",
"packages",
"python-contract-wrappers",
"generated",
"exchange",
"__init__.py",
),
path.join(
pkgdir, "src", "zero_ex", "contract_wrappers", "exchange"
),
path.join(pkgdir, "src", "zero_ex", "contract_wrappers"),
)
if find_spec("black") is None:
subprocess.check_call("pip install black".split()) # nosec
subprocess.check_call( # nosec
(
BLACK_COMMAND + " src/zero_ex/contract_wrappers/erc20_token.py"
).split()
black_command = (
BLACK_COMMAND
+ " src/zero_ex/contract_wrappers/erc20_token/__init__.py"
+ " src/zero_ex/contract_wrappers/exchange/__init__.py"
)
print(f"Running command `{black_command}`...")
subprocess.check_call(black_command.split()) # nosec
class TestCommandExtension(TestCommand):
@@ -112,6 +132,9 @@ class CleanCommandExtension(clean):
rmtree(".tox", ignore_errors=True)
rmtree(".pytest_cache", ignore_errors=True)
rmtree("src/0x_contract_wrappers.egg-info", ignore_errors=True)
# generated files:
remove("src/zero_ex/contract_wrappers/exchange/__init__.py")
remove("src/zero_ex/contract_wrappers/erc20_token/__init__.py")
class TestPublishCommand(distutils.command.build_py.build_py):
@@ -255,6 +278,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -31,6 +31,38 @@ zero_ex.contract_wrappers.TxParams
.. autoclass:: zero_ex.contract_wrappers.TxParams
:members:
zero_ex.contract_wrappers.exchange.types
========================================
.. automodule:: zero_ex.contract_wrappers.exchange.types
.. autoclass:: zero_ex.contract_wrappers.exchange.types.Order
.. autoclass:: zero_ex.contract_wrappers.exchange.types.OrderInfo
.. autoclass:: zero_ex.contract_wrappers.exchange.types.FillResults
.. autoclass:: zero_ex.contract_wrappers.exchange.types.MatchedFillResults
zero_ex.contract_wrappers.exchange: Generated Tuples
====================================================
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x260219a2
This is the generated class representing `the Order struct <https://0x.org/docs/contracts#structs-Order>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0xbb41e5b3
This is the generated class representing `the FillResults struct <https://0x.org/docs/contracts#structs-FillResults>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x054ca44e
This is the generated class representing `the MatchedFillResults struct <https://0x.org/docs/contracts#structs-MatchedFillResults>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0xb1e4a1ae
This is the generated class representing `the OrderInfo struct <https://0x.org/docs/contracts#structs-OrderInfo>`_.
Indices and tables
==================

View File

@@ -91,24 +91,24 @@ we need to tell the WETH token contract to let the 0x contracts transfer our
balance:
>>> from zero_ex.contract_wrappers import ERC20Token
>>> zrx_wrapper = ERC20Token(
>>> zrx_token = ERC20Token(
... provider=ganache,
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].zrx_token,
... )
>>> weth_wrapper = ERC20Token(
>>> weth_token = ERC20Token(
... provider=ganache,
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token,
... )
>>> erc20_proxy_addr = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].erc20_proxy
>>> tx = zrx_wrapper.approve(
>>> tx = zrx_token.approve(
... erc20_proxy_addr,
... to_wei(100, 'ether'),
... tx_params=TxParams(from_=maker_address),
... )
>>> tx = weth_wrapper.approve(
>>> tx = weth_token.approve(
... erc20_proxy_addr,
... to_wei(100, 'ether'),
... tx_params=TxParams(from_=taker_address),
@@ -117,8 +117,8 @@ balance:
Constructing an order
---------------------
>>> from zero_ex.order_utils import asset_data_utils, Order
>>> from eth_utils import remove_0x_prefix
>>> from zero_ex.contract_wrappers.exchange.types import Order
>>> from zero_ex.order_utils import asset_data_utils
>>> from datetime import datetime, timedelta
>>> import random
>>> order = Order(
@@ -143,8 +143,8 @@ For this order to be valid, our Maker must sign a hash of it:
>>> from zero_ex.order_utils import generate_order_hash_hex
>>> order_hash_hex = generate_order_hash_hex(order, exchange_address)
>>> from zero_ex.order_utils import sign_hash
>>> maker_signature = sign_hash(
>>> from zero_ex.order_utils import sign_hash_to_bytes
>>> maker_signature = sign_hash_to_bytes(
... ganache, Web3.toChecksumAddress(maker_address), order_hash_hex
... )
@@ -162,13 +162,13 @@ fill. This example fills the order completely, but partial fills are possible
too.
>>> from zero_ex.contract_wrappers import Exchange
>>> exchange_contract = Exchange(
>>> exchange = Exchange(
... provider=ganache,
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange,
... )
>>> tx_hash = exchange_contract.fill_order(
>>> tx_hash = exchange.fill_order(
... order=order,
... taker_amount=order["takerAssetAmount"],
... taker_asset_fill_amount=order["takerAssetAmount"],
... signature=maker_signature,
... tx_params=TxParams(from_=taker_address)
... )
@@ -176,10 +176,10 @@ too.
Once the transaction is mined, we can get the details of our exchange through
the exchange wrapper:
>>> exchange_contract.get_fill_event(tx_hash)
>>> exchange.get_fill_event(tx_hash)
(AttributeDict({'args': ...({'makerAddress': ...}), 'event': 'Fill', ...}),)
>>> from pprint import pprint
>>> pprint(exchange_contract.get_fill_event(tx_hash)[0].args.__dict__)
>>> pprint(exchange.get_fill_event(tx_hash)[0].args.__dict__)
{'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetData': b...,
@@ -191,7 +191,7 @@ the exchange wrapper:
'takerAssetData': b...,
'takerAssetFilledAmount': 100000000000000000,
'takerFeePaid': 0}
>>> exchange_contract.get_fill_event(tx_hash)[0].args.takerAssetFilledAmount
>>> exchange.get_fill_event(tx_hash)[0].args.takerAssetFilledAmount
100000000000000000
Cancelling an order
@@ -217,23 +217,23 @@ A Maker can cancel an order that has yet to be filled.
... )
... )
>>> tx_hash = exchange_contract.cancel_order(
>>> tx_hash = exchange.cancel_order(
... order=order, tx_params=TxParams(from_=maker_address)
... )
Once the transaction is mined, we can get the details of the cancellation
through the Exchange wrapper:
>>> exchange_contract.get_cancel_event(tx_hash)
>>> exchange.get_cancel_event(tx_hash)
(AttributeDict({'args': ...({'makerAddress': ...}), 'event': 'Cancel', ...}),)
>>> pprint(exchange_contract.get_cancel_event(tx_hash)[0].args.__dict__)
>>> pprint(exchange.get_cancel_event(tx_hash)[0].args.__dict__)
{'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetData': b...,
'orderHash': b...,
'senderAddress': '0x...',
'takerAssetData': b...}
>>> exchange_contract.get_cancel_event(tx_hash)[0].args.feeRecipientAddress
>>> exchange.get_cancel_event(tx_hash)[0].args.feeRecipientAddress
'0x0000000000000000000000000000000000000000'
Batching orders
@@ -258,10 +258,10 @@ is an example where the taker fills two orders in one transaction:
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... )
>>> signature_1 = sign_hash(
>>> signature_1 = sign_hash_to_bytes(
... ganache,
... Web3.toChecksumAddress(maker_address),
... generate_order_hash_hex(order_1, exchange_contract.address)
... generate_order_hash_hex(order_1, exchange.contract_address)
... )
>>> order_2 = Order(
... makerAddress=maker_address,
@@ -279,17 +279,17 @@ is an example where the taker fills two orders in one transaction:
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... )
>>> signature_2 = sign_hash(
>>> signature_2 = sign_hash_to_bytes(
... ganache,
... Web3.toChecksumAddress(maker_address),
... generate_order_hash_hex(order_2, exchange_contract.address)
... generate_order_hash_hex(order_2, exchange.contract_address)
... )
Fill order_1 and order_2 together:
>>> exchange_contract.batch_fill_orders(
>>> exchange.batch_fill_orders(
... orders=[order_1, order_2],
... taker_amounts=[1, 2],
... taker_asset_fill_amounts=[1, 2],
... signatures=[signature_1, signature_2],
... tx_params=TxParams(from_=taker_address))
HexBytes('0x...')
@@ -297,4 +297,4 @@ HexBytes('0x...')
from .tx_params import TxParams
from .erc20_token import ERC20Token
from .exchange_wrapper import Exchange
from .exchange import Exchange

View File

@@ -34,7 +34,7 @@ class BaseContractWrapper:
self._private_key = private_key
self._web3 = Web3(provider)
self._web3_eth = self._web3.eth # pylint: disable=no-member
self._contract_address = self._validate_and_checksum_address(
self.contract_address = self._validate_and_checksum_address(
contract_address
)
@@ -120,7 +120,7 @@ class BaseContractWrapper:
:returns: str of transaction hash
"""
contract_instance = self._contract_instance(
address=self._contract_address, abi=abi
address=self.contract_address, abi=abi
)
if args is None:
args = []
@@ -131,6 +131,6 @@ class BaseContractWrapper:
)
raise Exception(
"No method {} found on contract {}.".format(
self._contract_address, method
self.contract_address, method
)
)

View File

@@ -0,0 +1,200 @@
"""Conveniences for handling types representing Exchange Solidity structs.
The `TypedDict`:code: classes in the .exchange module represent tuples
encountered in the Exchange contract's ABI. However, they have weird names,
containing hashes of the tuple's field names, because the name of a Solidity
`struct`:code: isn't conveyed through the ABI. This module provides type
aliases with human-friendly names.
Converting between the JSON wire format and the types accepted by Web3.py (eg
`bytes` vs `str`) can be onerous. This module provides conveniences for
converting Exchange structs between JSON and Python objects.
"""
from copy import copy
from typing import cast, Dict
from eth_utils import remove_0x_prefix
from zero_ex.json_schemas import assert_valid
from . import (
Tuple0xbb41e5b3,
Tuple0x260219a2,
Tuple0x054ca44e,
Tuple0xb1e4a1ae,
)
# Would rather not repeat ourselves below, but base classes are mentioned in
# the class docstrings because of a bug in sphinx rendering. Using the `..
# autoclass` directive, with the `:show-inheritance:` role, results in docs
# being rendered with just "Bases: dict", and no mention of the direct ancestor
# of each of these classes.
class FillResults(Tuple0xbb41e5b3):
"""The `FillResults`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0xbb41e5b3`:py:class:.
"""
class Order(Tuple0x260219a2):
"""The `Order`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0x260219a2`:py:class:.
"""
class MatchedFillResults(Tuple0x054ca44e):
"""The `MatchedFillResults`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0x054ca44e`:py:class:.
"""
class OrderInfo(Tuple0xb1e4a1ae):
"""The `OrderInfo`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0xb1e4a1ae`:py:class:.
"""
def order_to_jsdict(
order: Order,
exchange_address="0x0000000000000000000000000000000000000000",
signature: str = None,
) -> dict:
"""Convert a Web3-compatible order struct to a JSON-schema-compatible dict.
More specifically, do explicit decoding for the `bytes`:code: fields, and
convert numerics to strings.
>>> import pprint
>>> pprint.pprint(order_to_jsdict(
... {
... 'makerAddress': "0x0000000000000000000000000000000000000000",
... 'takerAddress': "0x0000000000000000000000000000000000000000",
... 'feeRecipientAddress':
... "0x0000000000000000000000000000000000000000",
... 'senderAddress': "0x0000000000000000000000000000000000000000",
... 'makerAssetAmount': 1,
... 'takerAssetAmount': 1,
... 'makerFee': 0,
... 'takerFee': 0,
... 'expirationTimeSeconds': 1,
... 'salt': 1,
... 'makerAssetData': (0).to_bytes(1, byteorder='big') * 20,
... 'takerAssetData': (0).to_bytes(1, byteorder='big') * 20,
... },
... ))
{'exchangeAddress': '0x0000000000000000000000000000000000000000',
'expirationTimeSeconds': '1',
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x0000000000000000000000000000000000000000',
'makerAssetAmount': '1',
'makerAssetData': '0x0000000000000000000000000000000000000000',
'makerFee': '0',
'salt': '1',
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': '1',
'takerAssetData': '0x0000000000000000000000000000000000000000',
'takerFee': '0'}
"""
jsdict = cast(Dict, copy(order))
# encode bytes fields
jsdict["makerAssetData"] = "0x" + order["makerAssetData"].hex()
jsdict["takerAssetData"] = "0x" + order["takerAssetData"].hex()
jsdict["exchangeAddress"] = exchange_address
jsdict["expirationTimeSeconds"] = str(order["expirationTimeSeconds"])
jsdict["makerAssetAmount"] = str(order["makerAssetAmount"])
jsdict["takerAssetAmount"] = str(order["takerAssetAmount"])
jsdict["makerFee"] = str(order["makerFee"])
jsdict["takerFee"] = str(order["takerFee"])
jsdict["salt"] = str(order["salt"])
if signature is not None:
jsdict["signature"] = signature
assert_valid(jsdict, "/orderSchema")
return jsdict
def jsdict_to_order(jsdict: dict) -> Order:
r"""Convert a JSON-schema-compatible dict order to a Web3-compatible struct.
More specifically, do explicit encoding of the `bytes`:code: fields, and
parse integers from strings.
>>> import pprint
>>> pprint.pprint(jsdict_to_order(
... {
... 'makerAddress': "0x0000000000000000000000000000000000000000",
... 'takerAddress': "0x0000000000000000000000000000000000000000",
... 'feeRecipientAddress': "0x0000000000000000000000000000000000000000",
... 'senderAddress': "0x0000000000000000000000000000000000000000",
... 'makerAssetAmount': "1000000000000000000",
... 'takerAssetAmount': "1000000000000000000",
... 'makerFee': "0",
... 'takerFee': "0",
... 'expirationTimeSeconds': "12345",
... 'salt': "12345",
... 'makerAssetData': "0x0000000000000000000000000000000000000000",
... 'takerAssetData': "0x0000000000000000000000000000000000000000",
... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
... },
... ))
{'expirationTimeSeconds': 12345,
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x0000000000000000000000000000000000000000',
'makerAssetAmount': 1000000000000000000,
'makerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00',
'makerFee': 0,
'salt': 12345,
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': 1000000000000000000,
'takerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00',
'takerFee': 0}
""" # noqa: E501 (line too long)
assert_valid(jsdict, "/orderSchema")
order = cast(Order, copy(jsdict))
order["makerAssetData"] = bytes.fromhex(
remove_0x_prefix(jsdict["makerAssetData"])
)
order["takerAssetData"] = bytes.fromhex(
remove_0x_prefix(jsdict["takerAssetData"])
)
order["makerAssetAmount"] = int(jsdict["makerAssetAmount"])
order["takerAssetAmount"] = int(jsdict["takerAssetAmount"])
order["makerFee"] = int(jsdict["makerFee"])
order["takerFee"] = int(jsdict["takerFee"])
order["expirationTimeSeconds"] = int(jsdict["expirationTimeSeconds"])
order["salt"] = int(jsdict["salt"])
del order["exchangeAddress"] # type: ignore
# silence mypy pending release of
# https://github.com/python/mypy/issues/3550
return order

View File

@@ -0,0 +1,47 @@
"""Validate inputs to the Exchange contract."""
from typing import Any
from web3.providers.base import BaseProvider
from zero_ex import json_schemas
from . import ExchangeValidatorBase
from .types import order_to_jsdict
class ExchangeValidator(ExchangeValidatorBase):
"""Validate inputs to Exchange methods."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
"""Initialize the class."""
super().__init__(provider, contract_address, private_key)
self.contract_address = contract_address
def assert_valid(
self, method_name: str, parameter_name: str, argument_value: Any
) -> None:
"""Raise an exception if method input is not valid.
:param method_name: Name of the method whose input is to be validated.
:param parameter_name: Name of the parameter whose input is to be
validated.
:param argument_value: Value of argument to parameter to be validated.
"""
if parameter_name == "order":
json_schemas.assert_valid(
order_to_jsdict(argument_value, self.contract_address),
"/orderSchema",
)
if parameter_name == "orders":
for order in argument_value:
json_schemas.assert_valid(
order_to_jsdict(order, self.contract_address),
"/orderSchema",
)

View File

@@ -1,327 +0,0 @@
"""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."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
"""Get an instance of the 0x Exchange smart contract wrapper.
:param provider: instance of :class:`web3.providers.base.BaseProvider`
:param private_key: str of private_key
"""
super(Exchange, self).__init__(
provider=provider,
contract_address=contract_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")
)
# pylint: disable=too-many-arguments
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: 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
)
# pylint: disable=too-many-arguments
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: 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
)
# pylint: disable=too-many-arguments
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: 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
)
# pylint: disable=too-many-arguments
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: 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: 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: 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: fill event
"""
tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash)
return self._exchange.events.Fill().processReceipt(tx_receipt)
def get_cancel_event(
self, tx_hash: Union[HexBytes, bytes]
) -> Tuple[AttributeDict]:
"""Get cancel event for cancel transaction.
:param tx_hash: `HexBytes` hash of cancel transaction
"""
tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash)
return self._exchange.events.Cancel().processReceipt(tx_receipt)
@staticmethod
def _raise_if_maker_not_canceller(maker_address, canceller_address):
"""Raise exception is maker is not same as canceller."""
if maker_address != canceller_address:
raise CancelDisallowedError(
"Order with makerAddress {} can not be cancelled by {}".format(
maker_address, canceller_address
)
)

View File

@@ -7,8 +7,9 @@ from eth_utils import remove_0x_prefix
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
from zero_ex.contract_wrappers import Exchange, TxParams
from zero_ex.contract_wrappers.exchange.types import Order
from zero_ex.json_schemas import assert_valid
from zero_ex.order_utils import generate_order_hash_hex, Order, sign_hash
from zero_ex.order_utils import generate_order_hash_hex, sign_hash_to_bytes
@pytest.fixture(scope="module")
@@ -28,20 +29,20 @@ def create_test_order(
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,
}
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
@@ -69,16 +70,16 @@ def test_exchange_wrapper__fill_order(
"""Test filling an order."""
taker = accounts[0]
maker = accounts[1]
exchange_address = exchange_wrapper.address
exchange_address = exchange_wrapper.contract_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)
order_signature = sign_hash_to_bytes(ganache_provider, maker, order_hash)
tx_hash = exchange_wrapper.fill_order(
order=order,
taker_amount=order["takerAssetAmount"],
taker_asset_fill_amount=order["takerAssetAmount"],
signature=order_signature,
tx_params=TxParams(from_=taker),
)
@@ -98,7 +99,7 @@ def test_exchange_wrapper__batch_fill_orders(
"""Test filling a batch of orders."""
taker = accounts[0]
maker = accounts[1]
exchange_address = exchange_wrapper.address
exchange_address = exchange_wrapper.contract_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)
@@ -109,13 +110,13 @@ def test_exchange_wrapper__batch_fill_orders(
for order in orders
]
order_signatures = [
sign_hash(ganache_provider, maker, order_hash)
sign_hash_to_bytes(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,
taker_asset_fill_amounts=taker_amounts,
signatures=order_signatures,
tx_params=TxParams(from_=taker),
)

View File

@@ -1,13 +1,3 @@
#!/usr/bin/env python
#!/usr/bin/env bash
"""Script to install all packages in editable mode (pip install -e .)."""
from os import path
import subprocess
# install all packages
subprocess.check_call(
(
path.join(".", "cmd_pkgs_in_dep_order.py") + " pip install -e .[dev]"
).split()
)
./parallel pip install -e .[dev]

View File

@@ -5,7 +5,7 @@
import distutils.command.build_py
from distutils.command.clean import clean
import subprocess # nosec
from shutil import rmtree
from shutil import copytree, rmtree
from os import environ, path
from sys import argv
@@ -13,6 +13,26 @@ from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand
class PreInstallCommand(distutils.command.build_py.build_py):
"""Custom setuptools command class for pulling in schemas."""
description = "Pull in the schemas that live in the TypeScript package."
def run(self):
"""Copy files from TS area to local src."""
pkgdir = path.dirname(path.realpath(argv[0]))
rmtree(
path.join(pkgdir, "src", "zero_ex", "json_schemas", "schemas"),
ignore_errors=True,
)
copytree(
path.join(
pkgdir, "..", "..", "packages", "json-schemas", "schemas"
),
path.join(pkgdir, "src", "zero_ex", "json_schemas", "schemas"),
)
class TestCommandExtension(TestCommand):
"""Run pytest tests."""
@@ -41,15 +61,6 @@ class LintCommand(distutils.command.build_py.build_py):
"mypy src test setup.py".split(),
# security issue checker:
"bandit -r src ./setup.py".split(),
# HACK: ensure json schemas don't differ from the authoritative
# copies: this is a hack. ideally we would symlink to the
# authoritative copies, but a problem with setuptools is preventing
# it from following symlinks when gathering package_data. see
# https://github.com/pypa/setuptools/issues/415.
(
"diff src/zero_ex/json_schemas/schemas"
+ " ../../packages/json-schemas/schemas"
).split(),
# general linter:
"pylint src test setup.py".split(),
# pylint takes relatively long to run, so it runs last, to enable
@@ -138,6 +149,7 @@ setup(
author="F. Eugene Aumson",
author_email="feuGeneA@users.noreply.github.com",
cmdclass={
"pre_install": PreInstallCommand,
"clean": CleanCommandExtension,
"lint": LintCommand,
"test": TestCommandExtension,
@@ -196,6 +208,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -61,9 +61,10 @@ def assert_valid(data: Mapping, schema_id: str) -> None:
>>> from zero_ex.json_schemas import assert_valid
>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
>>> from zero_ex.order_utils import (
... asset_data_utils, Order, order_to_jsdict
>>> from zero_ex.contract_wrappers.exchange.types import (
... Order, order_to_jsdict
... )
>>> from zero_ex.order_utils import asset_data_utils
>>> from eth_utils import remove_0x_prefix
>>> import random
>>> from datetime import datetime, timedelta

View File

@@ -1,5 +0,0 @@
{
"id": "/addressSchema",
"type": "string",
"pattern": "^0x[0-9a-f]{40}$"
}

View File

@@ -1,8 +0,0 @@
{
"id": "/AssetPairsRequestOptsSchema",
"type": "object",
"properties": {
"assetDataA": { "$ref": "/hexSchema" },
"assetDataB": { "$ref": "/hexSchema" }
}
}

View File

@@ -1,11 +0,0 @@
{
"id": "/blockParamSchema",
"oneOf": [
{
"type": "number"
},
{
"enum": ["latest", "earliest", "pending"]
}
]
}

View File

@@ -1,8 +0,0 @@
{
"id": "/blockRangeSchema",
"properties": {
"fromBlock": { "$ref": "/blockParamSchema" },
"toBlock": { "$ref": "/blockParamSchema" }
},
"type": "object"
}

View File

@@ -1,27 +0,0 @@
{
"id": "/callDataSchema",
"properties": {
"from": { "$ref": "/addressSchema" },
"to": { "$ref": "/addressSchema" },
"value": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/jsNumberSchema" }]
},
"gas": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/jsNumberSchema" }]
},
"gasPrice": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/jsNumberSchema" }]
},
"data": {
"type": "string",
"pattern": "^0x[0-9a-f]*$"
},
"nonce": {
"type": "number",
"minimum": 0
}
},
"required": [],
"type": "object",
"additionalProperties": false
}

View File

@@ -1,5 +0,0 @@
{
"id": "/ecSignatureParameterSchema",
"type": "string",
"pattern": "^0[xX][0-9A-Fa-f]{64}$"
}

View File

@@ -1,14 +0,0 @@
{
"id": "/ecSignatureSchema",
"properties": {
"v": {
"type": "number",
"minimum": 27,
"maximum": 28
},
"r": { "$ref": "/ecSignatureParameterSchema" },
"s": { "$ref": "/ecSignatureParameterSchema" }
},
"required": ["v", "r", "s"],
"type": "object"
}

View File

@@ -1,28 +0,0 @@
{
"id": "/eip712TypedDataSchema",
"type": "object",
"properties": {
"types": {
"type": "object",
"properties": {
"EIP712Domain": { "type": "array" }
},
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "type": "string" }
},
"required": ["name", "type"]
}
},
"required": ["EIP712Domain"]
},
"primaryType": { "type": "string" },
"domain": { "type": "object" },
"message": { "type": "object" }
},
"required": ["types", "primaryType", "domain", "message"]
}

View File

@@ -1,5 +0,0 @@
{
"id": "/hexSchema",
"type": "string",
"pattern": "^0x(([0-9a-f][0-9a-f])+)?$"
}

View File

@@ -1,7 +0,0 @@
{
"id": "/indexFilterValuesSchema",
"additionalProperties": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/addressSchema" }, { "$ref": "/orderHashSchema" }]
},
"type": "object"
}

View File

@@ -1,5 +0,0 @@
{
"id": "/jsNumberSchema",
"type": "number",
"minimum": 0
}

View File

@@ -1,5 +0,0 @@
{
"id": "/numberSchema",
"type": "string",
"pattern": "^\\d+(\\.\\d+)?$"
}

View File

@@ -1,12 +0,0 @@
{
"id": "/orderCancellationRequestsSchema",
"type": "array",
"items": {
"properties": {
"order": { "$ref": "/orderSchema" },
"takerTokenCancelAmount": { "$ref": "/wholeNumberSchema" }
},
"required": ["order", "takerTokenCancelAmount"],
"type": "object"
}
}

View File

@@ -1,24 +0,0 @@
{
"id": "/OrderConfigRequestSchema",
"type": "object",
"properties": {
"makerAddress": { "$ref": "/addressSchema" },
"takerAddress": { "$ref": "/addressSchema" },
"makerAssetAmount": { "$ref": "/wholeNumberSchema" },
"takerAssetAmount": { "$ref": "/wholeNumberSchema" },
"makerAssetData": { "$ref": "/hexSchema" },
"takerAssetData": { "$ref": "/hexSchema" },
"exchangeAddress": { "$ref": "/addressSchema" },
"expirationTimeSeconds": { "$ref": "/wholeNumberSchema" }
},
"required": [
"makerAddress",
"takerAddress",
"makerAssetAmount",
"takerAssetAmount",
"makerAssetData",
"takerAssetData",
"exchangeAddress",
"expirationTimeSeconds"
]
}

View File

@@ -1,12 +0,0 @@
{
"id": "/orderFillOrKillRequestsSchema",
"type": "array",
"items": {
"properties": {
"signedOrder": { "$ref": "/signedOrderSchema" },
"fillTakerAmount": { "$ref": "/wholeNumberSchema" }
},
"required": ["signedOrder", "fillTakerAmount"],
"type": "object"
}
}

View File

@@ -1,12 +0,0 @@
{
"id": "/orderFillRequestsSchema",
"type": "array",
"items": {
"properties": {
"signedOrder": { "$ref": "/signedOrderSchema" },
"takerTokenFillAmount": { "$ref": "/wholeNumberSchema" }
},
"required": ["signedOrder", "takerTokenFillAmount"],
"type": "object"
}
}

View File

@@ -1,5 +0,0 @@
{
"id": "/orderHashSchema",
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
}

View File

@@ -1,34 +0,0 @@
{
"id": "/orderSchema",
"properties": {
"makerAddress": { "$ref": "/addressSchema" },
"takerAddress": { "$ref": "/addressSchema" },
"makerFee": { "$ref": "/wholeNumberSchema" },
"takerFee": { "$ref": "/wholeNumberSchema" },
"senderAddress": { "$ref": "/addressSchema" },
"makerAssetAmount": { "$ref": "/wholeNumberSchema" },
"takerAssetAmount": { "$ref": "/wholeNumberSchema" },
"makerAssetData": { "$ref": "/hexSchema" },
"takerAssetData": { "$ref": "/hexSchema" },
"salt": { "$ref": "/wholeNumberSchema" },
"exchangeAddress": { "$ref": "/addressSchema" },
"feeRecipientAddress": { "$ref": "/addressSchema" },
"expirationTimeSeconds": { "$ref": "/wholeNumberSchema" }
},
"required": [
"makerAddress",
"takerAddress",
"makerFee",
"takerFee",
"senderAddress",
"makerAssetAmount",
"takerAssetAmount",
"makerAssetData",
"takerAssetData",
"salt",
"exchangeAddress",
"feeRecipientAddress",
"expirationTimeSeconds"
],
"type": "object"
}

View File

@@ -1,52 +0,0 @@
{
"id": "/orderWatcherWebSocketRequestSchema",
"type": "object",
"definitions": {
"signedOrderParam": {
"type": "object",
"properties": {
"signedOrder": { "$ref": "/signedOrderSchema" }
},
"required": ["signedOrder"]
},
"orderHashParam": {
"type": "object",
"properties": {
"orderHash": { "$ref": "/hexSchema" }
},
"required": ["orderHash"]
}
},
"oneOf": [
{
"type": "object",
"properties": {
"id": { "type": "number" },
"jsonrpc": { "type": "string" },
"method": { "enum": ["ADD_ORDER"] },
"params": { "$ref": "#/definitions/signedOrderParam" }
},
"required": ["id", "jsonrpc", "method", "params"]
},
{
"type": "object",
"properties": {
"id": { "type": "number" },
"jsonrpc": { "type": "string" },
"method": { "enum": ["REMOVE_ORDER"] },
"params": { "$ref": "#/definitions/orderHashParam" }
},
"required": ["id", "jsonrpc", "method", "params"]
},
{
"type": "object",
"properties": {
"id": { "type": "number" },
"jsonrpc": { "type": "string" },
"method": { "enum": ["GET_STATS"] },
"params": {}
},
"required": ["id", "jsonrpc", "method"]
}
]
}

View File

@@ -1,10 +0,0 @@
{
"id": "/orderWatcherWebSocketUtf8MessageSchema",
"properties": {
"utf8Data": { "type": "string" }
},
"required": [
"utf8Data"
],
"type": "object"
}

View File

@@ -1,9 +0,0 @@
{
"id": "/OrderbookRequestSchema",
"type": "object",
"properties": {
"baseAssetData": { "$ref": "/hexSchema" },
"quoteAssetData": { "$ref": "/hexSchema" }
},
"required": ["baseAssetData", "quoteAssetData"]
}

View File

@@ -1,19 +0,0 @@
{
"id": "/OrdersRequestOptsSchema",
"type": "object",
"properties": {
"makerAssetProxyId": { "$ref": "/hexSchema" },
"takerAssetProxyId": { "$ref": "/hexSchema" },
"makerAssetAddress": { "$ref": "/addressSchema" },
"takerAssetAddress": { "$ref": "/addressSchema" },
"exchangeAddress": { "$ref": "/addressSchema" },
"senderAddress": { "$ref": "/addressSchema" },
"makerAssetData": { "$ref": "/hexSchema" },
"takerAssetData": { "$ref": "/hexSchema" },
"traderAssetData": { "$ref": "/hexSchema" },
"makerAddress": { "$ref": "/addressSchema" },
"takerAddress": { "$ref": "/addressSchema" },
"traderAddress": { "$ref": "/addressSchema" },
"feeRecipientAddress": { "$ref": "/addressSchema" }
}
}

View File

@@ -1,5 +0,0 @@
{
"id": "/ordersSchema",
"type": "array",
"items": { "$ref": "/orderSchema" }
}

View File

@@ -1,8 +0,0 @@
{
"id": "/PagedRequestOptsSchema",
"type": "object",
"properties": {
"page": { "type": "number" },
"perPage": { "type": "number" }
}
}

View File

@@ -1,10 +0,0 @@
{
"id": "/paginatedCollectionSchema",
"type": "object",
"properties": {
"total": { "type": "number" },
"perPage": { "type": "number" },
"page": { "type": "number" }
},
"required": ["total", "perPage", "page"]
}

View File

@@ -1,13 +0,0 @@
{
"id": "/relayerApiAssetDataPairsResponseSchema",
"type": "object",
"allOf": [
{ "$ref": "/paginatedCollectionSchema" },
{
"properties": {
"records": { "$ref": "/relayerApiAssetDataPairsSchema" }
},
"required": ["records"]
}
]
}

View File

@@ -1,12 +0,0 @@
{
"id": "/relayerApiAssetDataPairsSchema",
"type": "array",
"items": {
"properties": {
"assetDataA": { "$ref": "/relayerApiAssetDataTradeInfoSchema" },
"assetDataB": { "$ref": "/relayerApiAssetDataTradeInfoSchema" }
},
"required": ["assetDataA", "assetDataB"],
"type": "object"
}
}

View File

@@ -1,11 +0,0 @@
{
"id": "/relayerApiAssetDataTradeInfoSchema",
"type": "object",
"properties": {
"assetData": { "$ref": "/hexSchema" },
"minAmount": { "$ref": "/wholeNumberSchema" },
"maxAmount": { "$ref": "/wholeNumberSchema" },
"precision": { "type": "number" }
},
"required": ["assetData"]
}

View File

@@ -1,21 +0,0 @@
{
"id": "/relayerApiErrorResponseSchema",
"type": "object",
"properties": {
"code": { "type": "integer", "minimum": 100, "maximum": 103 },
"reason": { "type": "string" },
"validationErrors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": { "type": "string" },
"code": { "type": "integer", "minimum": 1000, "maximum": 1006 },
"reason": { "type": "string" }
},
"required": ["field", "code", "reason"]
}
}
},
"required": ["code", "reason"]
}

View File

@@ -1,16 +0,0 @@
{
"id": "/relayerApiFeeRecipientsResponseSchema",
"type": "object",
"allOf": [
{ "$ref": "/paginatedCollectionSchema" },
{
"properties": {
"records": {
"type": "array",
"items": { "$ref": "/addressSchema" }
}
},
"required": ["records"]
}
]
}

View File

@@ -1,24 +0,0 @@
{
"id": "/relayerApiOrderConfigPayloadSchema",
"type": "object",
"properties": {
"makerAddress": { "$ref": "/addressSchema" },
"takerAddress": { "$ref": "/addressSchema" },
"makerAssetAmount": { "$ref": "/wholeNumberSchema" },
"takerAssetAmount": { "$ref": "/wholeNumberSchema" },
"makerAssetData": { "$ref": "/hexSchema" },
"takerAssetData": { "$ref": "/hexSchema" },
"exchangeAddress": { "$ref": "/addressSchema" },
"expirationTimeSeconds": { "$ref": "/wholeNumberSchema" }
},
"required": [
"makerAddress",
"takerAddress",
"makerAssetAmount",
"takerAssetAmount",
"makerAssetData",
"takerAssetData",
"exchangeAddress",
"expirationTimeSeconds"
]
}

View File

@@ -1,11 +0,0 @@
{
"id": "/relayerApiOrderConfigResponseSchema",
"type": "object",
"properties": {
"makerFee": { "$ref": "/wholeNumberSchema" },
"takerFee": { "$ref": "/wholeNumberSchema" },
"feeRecipientAddress": { "$ref": "/addressSchema" },
"senderAddress": { "$ref": "/addressSchema" }
},
"required": ["makerFee", "takerFee", "feeRecipientAddress", "senderAddress"]
}

View File

@@ -1,9 +0,0 @@
{
"id": "/relayerApiOrderSchema",
"type": "object",
"properties": {
"order": { "$ref": "/orderSchema" },
"metaData": { "type": "object" }
},
"required": ["order", "metaData"]
}

View File

@@ -1,9 +0,0 @@
{
"id": "/relayerApiOrderbookResponseSchema",
"type": "object",
"properties": {
"bids": { "$ref": "/relayerApiOrdersResponseSchema" },
"asks": { "$ref": "/relayerApiOrdersResponseSchema" }
},
"required": ["bids", "asks"]
}

View File

@@ -1,14 +0,0 @@
{
"id": "/relayerApiOrdersChannelSubscribePayloadSchema",
"type": "object",
"properties": {
"makerAssetProxyId": { "$ref": "/hexSchema" },
"takerAssetProxyId": { "$ref": "/hexSchema" },
"networkId": { "type": "number" },
"makerAssetAddress": { "$ref": "/addressSchema" },
"takerAssetAddress": { "$ref": "/addressSchema" },
"makerAssetData": { "$ref": "/hexSchema" },
"takerAssetData": { "$ref": "/hexSchema" },
"traderAssetData": { "$ref": "/hexSchema" }
}
}

View File

@@ -1,11 +0,0 @@
{
"id": "/relayerApiOrdersChannelSubscribeSchema",
"type": "object",
"properties": {
"type": { "enum": ["subscribe"] },
"channel": { "enum": ["orders"] },
"requestId": { "type": "string" },
"payload": { "$ref": "/relayerApiOrdersChannelSubscribePayloadSchema" }
},
"required": ["type", "channel", "requestId"]
}

View File

@@ -1,11 +0,0 @@
{
"id": "/relayerApiOrdersChannelUpdateSchema",
"type": "object",
"properties": {
"type": { "enum": ["update"] },
"channel": { "enum": ["orders"] },
"requestId": { "type": "string" },
"payload": { "$ref": "/relayerApiOrdersSchema" }
},
"required": ["type", "channel", "requestId"]
}

View File

@@ -1,13 +0,0 @@
{
"id": "/relayerApiOrdersResponseSchema",
"type": "object",
"allOf": [
{ "$ref": "/paginatedCollectionSchema" },
{
"properties": {
"records": { "$ref": "/relayerApiOrdersSchema" }
},
"required": ["records"]
}
]
}

View File

@@ -1,5 +0,0 @@
{
"id": "/relayerApiOrdersSchema",
"type": "array",
"items": { "$ref": "/relayerApiOrderSchema" }
}

View File

@@ -1,7 +0,0 @@
{
"id": "/RequestOptsSchema",
"type": "object",
"properties": {
"networkId": { "type": "number" }
}
}

View File

@@ -1,12 +0,0 @@
{
"id": "/signedOrderSchema",
"allOf": [
{ "$ref": "/orderSchema" },
{
"properties": {
"signature": { "$ref": "/hexSchema" }
},
"required": ["signature"]
}
]
}

View File

@@ -1,5 +0,0 @@
{
"id": "/signedOrdersSchema",
"type": "array",
"items": { "$ref": "/signedOrderSchema" }
}

View File

@@ -1,11 +0,0 @@
{
"id": "/tokenSchema",
"properties": {
"name": { "type": "string" },
"symbol": { "type": "string" },
"decimals": { "type": "number" },
"address": { "$ref": "/addressSchema" }
},
"required": ["name", "symbol", "decimals", "address"],
"type": "object"
}

View File

@@ -1,26 +0,0 @@
{
"id": "/txDataSchema",
"properties": {
"from": { "$ref": "/addressSchema" },
"to": { "$ref": "/addressSchema" },
"value": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/jsNumberSchema" }]
},
"gas": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/jsNumberSchema" }]
},
"gasPrice": {
"oneOf": [{ "$ref": "/numberSchema" }, { "$ref": "/jsNumberSchema" }]
},
"data": {
"type": "string",
"pattern": "^0x[0-9a-f]*$"
},
"nonce": {
"type": "number",
"minimum": 0
}
},
"required": ["from"],
"type": "object"
}

View File

@@ -1,12 +0,0 @@
{
"id": "/wholeNumberSchema",
"anyOf": [
{
"type": "string",
"pattern": "^\\d+$"
},
{
"type": "integer"
}
]
}

View File

@@ -1,11 +0,0 @@
{
"id": "/zeroExTransactionSchema",
"properties": {
"verifyingContractAddress": { "$ref": "/addressSchema" },
"data": { "$ref": "/hexSchema" },
"signerAddress": { "$ref": "/addressSchema" },
"salt": { "$ref": "/wholeNumberSchema" }
},
"required": ["verifyingContractAddress", "data", "salt", "signerAddress"],
"type": "object"
}

View File

@@ -1,7 +1,6 @@
"""Tests of zero_ex.json_schemas"""
from zero_ex.order_utils import make_empty_order, order_to_jsdict
from zero_ex.json_schemas import _LOCAL_RESOLVER, assert_valid
@@ -34,7 +33,7 @@ def test_assert_valid_caches_resources():
"""
_LOCAL_RESOLVER._remote_cache.cache_clear() # pylint: disable=W0212
assert_valid(order_to_jsdict(make_empty_order()), "/orderSchema")
assert_valid(EMPTY_ORDER, "/orderSchema")
cache_info = (
_LOCAL_RESOLVER._remote_cache.cache_info() # pylint: disable=W0212
)

View File

@@ -1,13 +1,3 @@
#!/usr/bin/env python
#!/usr/bin/env bash
"""Script to run linters against local copy of all components."""
from os import path
import subprocess
subprocess.check_call(
(
f"{path.join('.', 'cmd_pkgs_in_dep_order.py')}"
+ f" {path.join('.', 'setup.py')} lint"
).split()
)
./parallel ./setup.py lint

View File

@@ -212,6 +212,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -10,7 +10,6 @@ from zero_ex.middlewares.local_message_signer import (
from zero_ex.order_utils import (
generate_order_hash_hex,
is_valid_signature,
make_empty_order,
sign_hash,
)
@@ -31,7 +30,20 @@ def test_local_message_signer__sign_order():
web3_instance.middleware_stack.add(
construct_local_message_signer(private_key)
)
order = make_empty_order()
order = {
"makerAddress": "0x0000000000000000000000000000000000000000",
"takerAddress": "0x0000000000000000000000000000000000000000",
"senderAddress": "0x0000000000000000000000000000000000000000",
"feeRecipientAddress": "0x0000000000000000000000000000000000000000",
"makerAssetData": (b"\x00") * 20,
"takerAssetData": (b"\x00") * 20,
"salt": 0,
"makerFee": 0,
"takerFee": 0,
"makerAssetAmount": 0,
"takerAssetAmount": 0,
"expirationTimeSeconds": 0,
}
order_hash = generate_order_hash_hex(order, exchange)
signature = sign_hash(ganache, to_checksum_address(address), order_hash)
assert signature == expected_signature

View File

@@ -1,8 +1,8 @@
# Changelog
## 1.1.1 - 2019-02-26
## 3.0.0 - TBD
- Replaced dependency on web3 with dependency on 0x-web3, to ease coexistence of those two packages.
- Major breaking changes: removal of definitions for Order, OrderInfo, order_to_jsdict, jsdict_to_order, all of which have been moved to contract_wrappers.exchange.types; removal of signature validation.
## 2.0.0 - 2019-04-30
@@ -10,3 +10,7 @@
- Deprecated methods `encode_erc20_asset_data()` and `encode_erc721_asset_data()`, in favor of new methods `encode_erc20()` and `encode_erc721()`. The old methods return a string, which is less than convenient for building orders using the provided `Order` type, which expects asset data to be `bytes`. The new methods return `bytes`.
- Expanded documentation.
- Stopped using deprecated web3.py interface `contract.call()` in favor of `contract.functions.X.call()`. This provides compatibility with the upcoming 5.x release of web3.py, and it also eliminates some runtime warning messages.
## 1.1.1 - 2019-02-26
- Replaced dependency on web3 with dependency on 0x-web3, to ease coexistence of those two packages.

View File

@@ -229,6 +229,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -10,10 +10,6 @@ Python zero_ex.order_utils
.. automodule:: zero_ex.order_utils
:members:
.. autoclass:: zero_ex.order_utils.Order
See source for class properties. Sphinx is having problems generating docs for ``TypedDict`` declarations; pull requests welcome.
zero_ex.order_utils.asset_data_utils
------------------------------------

View File

@@ -14,58 +14,11 @@ contracts deployed on it. For convenience, a docker container is provided for
just this purpose. To start it:
`docker run -d -p 8545:8545 0xorg/ganache-cli:2.2.2`:code:.
Constructing an order
---------------------
"""
Here is a short demonstration on how to create a 0x order.
>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
>>> from zero_ex.order_utils import asset_data_utils, Order
>>> from datetime import datetime, timedelta
>>> import random
>>> my_address = "0x5409ed021d9299bf6814279a6a1411a7e866a631"
>>> example_order = Order(
... makerAddress=my_address,
... takerAddress="0x0000000000000000000000000000000000000000",
... exchangeAddress=NETWORK_TO_ADDRESSES[NetworkId.MAINNET].exchange,
... senderAddress="0x0000000000000000000000000000000000000000",
... feeRecipientAddress="0x0000000000000000000000000000000000000000",
... makerAssetData=asset_data_utils.encode_erc20(
... NETWORK_TO_ADDRESSES[NetworkId.MAINNET].ether_token
... ),
... takerAssetData=asset_data_utils.encode_erc20(
... NETWORK_TO_ADDRESSES[NetworkId.MAINNET].zrx_token
... ),
... salt=random.randint(1, 100000000000000000),
... makerFee=0,
... takerFee=0,
... makerAssetAmount=1 * 10 ** 18, # Convert token amount to base unit with 18 decimals
... takerAssetAmount=500 * 10 ** 18, # Convert token amount to base unit with 18 decimals
... expirationTimeSeconds=round(
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... )
>>> import pprint
>>> pprint.pprint(example_order)
{'exchangeAddress': '0x...',
'expirationTimeSeconds': ...,
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',
'makerAssetAmount': 1000000000000000000,
'makerAssetData': b...,
'makerFee': 0,
'salt': ...,
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': 500000000000000000000,
'takerAssetData': b...,
'takerFee': 0}
""" # noqa E501
from copy import copy
from enum import auto, Enum
import json
from typing import cast, Dict, NamedTuple, Tuple
from typing import Tuple
from pkg_resources import resource_string
from mypy_extensions import TypedDict
@@ -78,6 +31,7 @@ from web3.utils import datatypes
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
import zero_ex.contract_artifacts
from zero_ex.contract_wrappers.exchange.types import Order, order_to_jsdict
from zero_ex.dev_utils.type_assertions import (
assert_is_address,
assert_is_hex_string,
@@ -133,238 +87,6 @@ class _Constants:
N_SIGNATURE_TYPES = auto()
class Order(TypedDict): # pylint: disable=too-many-instance-attributes
"""A Web3-compatible representation of the Exchange.Order struct.
>>> from zero_ex.order_utils import asset_data_utils
>>> from eth_utils import remove_0x_prefix
>>> from datetime import datetime, timedelta
>>> import random
>>> order = Order(
... makerAddress=maker_address,
... takerAddress='0x0000000000000000000000000000000000000000',
... senderAddress='0x0000000000000000000000000000000000000000',
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
... makerAssetData=asset_data_utils.encode_erc20(zrx_address),
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
... salt=random.randint(1, 100000000000000000),
... makerFee=0,
... takerFee=0,
... makerAssetAmount=1,
... takerAssetAmount=1,
... expirationTimeSeconds=round(
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... )
"""
makerAddress: str
"""Address that created the order."""
takerAddress: str
"""Address that is allowed to fill the order.
If set to 0, any address is allowed to fill the order.
"""
feeRecipientAddress: str
"""Address that will recieve fees when order is filled."""
senderAddress: str
"""Address that is allowed to call Exchange contract methods that affect
this order. If set to 0, any address is allowed to call these methods.
"""
makerAssetAmount: int
"""Amount of makerAsset being offered by maker. Must be greater than 0."""
takerAssetAmount: int
"""Amount of takerAsset being bid on by maker. Must be greater than 0."""
makerFee: int
"""Amount of ZRX paid to feeRecipient by maker when order is filled. If
set to 0, no transfer of ZRX from maker to feeRecipient will be attempted.
"""
takerFee: int
"""Amount of ZRX paid to feeRecipient by taker when order is filled. If
set to 0, no transfer of ZRX from taker to feeRecipient will be attempted.
"""
expirationTimeSeconds: int
"""Timestamp in seconds at which order expires."""
salt: int
"""Arbitrary number to facilitate uniqueness of the order's hash."""
makerAssetData: bytes
"""Encoded data that can be decoded by a specified proxy contract when
transferring makerAsset. The last byte references the id of this proxy.
"""
takerAssetData: bytes
"""Encoded data that can be decoded by a specified proxy contract when
transferring takerAsset. The last byte references the id of this proxy.
"""
def make_empty_order() -> Order:
"""Construct an empty order.
Initializes all strings to "0x0000000000000000000000000000000000000000",
all numbers to 0, and all bytes to nulls.
"""
return {
"makerAddress": _Constants.null_address,
"takerAddress": _Constants.null_address,
"senderAddress": _Constants.null_address,
"feeRecipientAddress": _Constants.null_address,
"makerAssetData": (b"\x00") * 20,
"takerAssetData": (b"\x00") * 20,
"salt": 0,
"makerFee": 0,
"takerFee": 0,
"makerAssetAmount": 0,
"takerAssetAmount": 0,
"expirationTimeSeconds": 0,
}
def order_to_jsdict(
order: Order,
exchange_address="0x0000000000000000000000000000000000000000",
signature: str = None,
) -> dict:
"""Convert a Web3-compatible order struct to a JSON-schema-compatible dict.
More specifically, do explicit decoding for the `bytes`:code: fields, and
convert numerics to strings.
>>> import pprint
>>> pprint.pprint(order_to_jsdict(
... {
... 'makerAddress': "0x0000000000000000000000000000000000000000",
... 'takerAddress': "0x0000000000000000000000000000000000000000",
... 'feeRecipientAddress':
... "0x0000000000000000000000000000000000000000",
... 'senderAddress': "0x0000000000000000000000000000000000000000",
... 'makerAssetAmount': 1,
... 'takerAssetAmount': 1,
... 'makerFee': 0,
... 'takerFee': 0,
... 'expirationTimeSeconds': 1,
... 'salt': 1,
... 'makerAssetData': (0).to_bytes(1, byteorder='big') * 20,
... 'takerAssetData': (0).to_bytes(1, byteorder='big') * 20,
... },
... ))
{'exchangeAddress': '0x0000000000000000000000000000000000000000',
'expirationTimeSeconds': '1',
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x0000000000000000000000000000000000000000',
'makerAssetAmount': '1',
'makerAssetData': '0x0000000000000000000000000000000000000000',
'makerFee': '0',
'salt': '1',
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': '1',
'takerAssetData': '0x0000000000000000000000000000000000000000',
'takerFee': '0'}
"""
jsdict = cast(Dict, copy(order))
# encode bytes fields
jsdict["makerAssetData"] = "0x" + order["makerAssetData"].hex()
jsdict["takerAssetData"] = "0x" + order["takerAssetData"].hex()
jsdict["exchangeAddress"] = exchange_address
jsdict["expirationTimeSeconds"] = str(order["expirationTimeSeconds"])
jsdict["makerAssetAmount"] = str(order["makerAssetAmount"])
jsdict["takerAssetAmount"] = str(order["takerAssetAmount"])
jsdict["makerFee"] = str(order["makerFee"])
jsdict["takerFee"] = str(order["takerFee"])
jsdict["salt"] = str(order["salt"])
if signature is not None:
jsdict["signature"] = signature
assert_valid(jsdict, "/orderSchema")
return jsdict
def jsdict_to_order(jsdict: dict) -> Order:
r"""Convert a JSON-schema-compatible dict order to a Web3-compatible struct.
More specifically, do explicit encoding of the `bytes`:code: fields, and
parse integers from strings.
>>> import pprint
>>> pprint.pprint(jsdict_to_order(
... {
... 'makerAddress': "0x0000000000000000000000000000000000000000",
... 'takerAddress': "0x0000000000000000000000000000000000000000",
... 'feeRecipientAddress': "0x0000000000000000000000000000000000000000",
... 'senderAddress': "0x0000000000000000000000000000000000000000",
... 'makerAssetAmount': "1000000000000000000",
... 'takerAssetAmount': "1000000000000000000",
... 'makerFee': "0",
... 'takerFee': "0",
... 'expirationTimeSeconds': "12345",
... 'salt': "12345",
... 'makerAssetData': "0x0000000000000000000000000000000000000000",
... 'takerAssetData': "0x0000000000000000000000000000000000000000",
... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
... },
... ))
{'expirationTimeSeconds': 12345,
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x0000000000000000000000000000000000000000',
'makerAssetAmount': 1000000000000000000,
'makerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00',
'makerFee': 0,
'salt': 12345,
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': 1000000000000000000,
'takerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00',
'takerFee': 0}
""" # noqa: E501 (line too long)
assert_valid(jsdict, "/orderSchema")
order = cast(Order, copy(jsdict))
order["makerAssetData"] = bytes.fromhex(
remove_0x_prefix(jsdict["makerAssetData"])
)
order["takerAssetData"] = bytes.fromhex(
remove_0x_prefix(jsdict["takerAssetData"])
)
order["makerAssetAmount"] = int(jsdict["makerAssetAmount"])
order["takerAssetAmount"] = int(jsdict["takerAssetAmount"])
order["makerFee"] = int(jsdict["makerFee"])
order["takerFee"] = int(jsdict["takerFee"])
order["expirationTimeSeconds"] = int(jsdict["expirationTimeSeconds"])
order["salt"] = int(jsdict["salt"])
del order["exchangeAddress"] # type: ignore
# silence mypy pending release of
# https://github.com/python/mypy/issues/3550
return order
def generate_order_hash_hex(order: Order, exchange_address: str) -> str:
"""Calculate the hash of the given order as a hexadecimal string.
@@ -374,20 +96,20 @@ def generate_order_hash_hex(order: Order, exchange_address: str) -> str:
:returns: A string, of ASCII hex digits, representing the order hash.
>>> generate_order_hash_hex(
... {
... 'makerAddress': "0x0000000000000000000000000000000000000000",
... 'takerAddress': "0x0000000000000000000000000000000000000000",
... 'feeRecipientAddress': "0x0000000000000000000000000000000000000000",
... 'senderAddress': "0x0000000000000000000000000000000000000000",
... 'makerAssetAmount': "1000000000000000000",
... 'takerAssetAmount': "1000000000000000000",
... 'makerFee': "0",
... 'takerFee': "0",
... 'expirationTimeSeconds': "12345",
... 'salt': "12345",
... 'makerAssetData': (0).to_bytes(1, byteorder='big') * 20,
... 'takerAssetData': (0).to_bytes(1, byteorder='big') * 20,
... },
... Order(
... makerAddress="0x0000000000000000000000000000000000000000",
... takerAddress="0x0000000000000000000000000000000000000000",
... feeRecipientAddress="0x0000000000000000000000000000000000000000",
... senderAddress="0x0000000000000000000000000000000000000000",
... makerAssetAmount="1000000000000000000",
... takerAssetAmount="1000000000000000000",
... makerFee="0",
... takerFee="0",
... expirationTimeSeconds="12345",
... salt="12345",
... makerAssetData=((0).to_bytes(1, byteorder='big') * 20),
... takerAssetData=((0).to_bytes(1, byteorder='big') * 20),
... ),
... exchange_address="0x0000000000000000000000000000000000000000",
... )
'55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692'
@@ -429,19 +151,6 @@ def generate_order_hash_hex(order: Order, exchange_address: str) -> str:
).hex()
class OrderInfo(NamedTuple):
"""A Web3-compatible representation of the Exchange.OrderInfo struct."""
order_status: str
"""A `str`:code: describing the order's validity and fillability."""
order_hash: bytes
"""A `bytes`:code: object representing the EIP712 hash of the order."""
order_taker_asset_filled_amount: int
"""An `int`:code: indicating the amount that has already been filled."""
def is_valid_signature(
provider: BaseProvider, data: str, signature: str, signer_address: str
) -> Tuple[bool, str]:
@@ -636,3 +345,21 @@ def sign_hash(
"Signature returned from web3 provider is in an unknown format."
+ " Attempted to parse as RSV and as VRS."
)
def sign_hash_to_bytes(
provider: BaseProvider, signer_address: str, hash_hex: str
) -> bytes:
"""Sign a message with the given hash, and return the signature.
>>> provider = Web3.HTTPProvider("http://127.0.0.1:8545")
>>> sign_hash_to_bytes(
... provider,
... Web3(provider).personal.listAccounts[0],
... '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004',
... ).decode(encoding='utf_8')
'1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03'
""" # noqa: E501 (line too long)
return remove_0x_prefix(
sign_hash(provider, signer_address, hash_hex)
).encode(encoding="utf_8")

View File

@@ -1,6 +1,6 @@
"""Test zero_ex.order_utils.get_order_hash_hex()."""
from zero_ex.order_utils import generate_order_hash_hex, make_empty_order
from zero_ex.order_utils import generate_order_hash_hex
def test_get_order_hash_hex__empty_order():
@@ -9,6 +9,22 @@ def test_get_order_hash_hex__empty_order():
"faa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422"
)
actual_hash_hex = generate_order_hash_hex(
make_empty_order(), "0x0000000000000000000000000000000000000000"
{
"makerAddress": "0x0000000000000000000000000000000000000000",
"takerAddress": "0x0000000000000000000000000000000000000000",
"senderAddress": "0x0000000000000000000000000000000000000000",
"feeRecipientAddress": (
"0x0000000000000000000000000000000000000000"
),
"makerAssetData": (b"\x00") * 20,
"takerAssetData": (b"\x00") * 20,
"salt": 0,
"makerFee": 0,
"takerFee": 0,
"makerAssetAmount": 0,
"takerAssetAmount": 0,
"expirationTimeSeconds": 0,
},
"0x0000000000000000000000000000000000000000",
)
assert actual_hash_hex == expected_hash_hex

View File

@@ -3,7 +3,7 @@
import pytest
from web3 import Web3
from zero_ex.order_utils import is_valid_signature
from zero_ex.order_utils import is_valid_signature, sign_hash_to_bytes
def test_is_valid_signature__provider_wrong_type():
@@ -126,3 +126,17 @@ def test_is_valid_signature__unsupported_sig_types():
)
assert is_valid is False
assert reason == "SIGNATURE_UNSUPPORTED"
def test_sign_hash_to_bytes__golden_path():
"""Test the happy path through sign_hash_to_bytes()."""
provider = Web3.HTTPProvider("http://127.0.0.1:8545")
signature = sign_hash_to_bytes(
provider,
Web3(provider).personal.listAccounts[0], # pylint: disable=no-member
"0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004",
)
assert (
signature
== b"1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03" # noqa: E501 (line too long)
)

44
python-packages/parallel Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python
"""Run the given command in all packages in parallel.
Handy for quick verification test runs, but annoying in that all of the output
is interleaved.
$ ./parallel ./setup.py lint
This will `cd` into each package, run `./setup.py lint`, then `cd ..`, all in
parallel, in a separate process for each package. The number of processes is
decided by ProcessPoolExecutor. Replace "lint" with any of "test", "clean",
"build_sphinx" (for docs), etc.
Also consider:
$ ./parallel pip install -e .[dev] # install all the packages in editable mode
$ ./parallel pip uninstall $(basename $(pwd))
>>>"""
from concurrent.futures import ProcessPoolExecutor
from os import chdir
from subprocess import check_call
from sys import argv
PACKAGES = [
"contract_addresses",
"contract_artifacts",
"contract_wrappers",
"json_schemas",
"sra_client",
"order_utils",
"middlewares",
]
def run_cmd_on_package(package: str):
"""cd to the package dir, ./setup.py lint, cd .."""
chdir(package)
check_call(f"{' '.join(argv[1:])}".split())
chdir("..")
ProcessPoolExecutor().map(run_cmd_on_package, PACKAGES)

View File

@@ -5,17 +5,21 @@
from os import chdir, path
import subprocess
from concurrent.futures import ProcessPoolExecutor
from os import chdir
from subprocess import check_call
from sys import argv
PACKAGES = [
"contract_wrappers",
"contract_artifacts",
"json_schemas",
]
for package in PACKAGES:
print(f"Running command `pre_install` in package {package}")
def run_cmd_on_package(package: str):
"""cd to the package dir, ./setup.py pre_install, cd .."""
chdir(package)
subprocess.check_call(
(
path.join(".", "setup.py") + " pre_install"
).split()
)
check_call(f"{path.join('.', 'setup.py')} pre_install".split())
chdir("..")
ProcessPoolExecutor().map(run_cmd_on_package, PACKAGES)

View File

@@ -193,6 +193,7 @@ setup(
"build_sphinx": {
"source_dir": ("setup.py", "src"),
"build_dir": ("setup.py", "build/docs"),
"warning_is_error": ("setup.py", "true"),
}
},
)

View File

@@ -105,10 +105,9 @@ Post Order
Post an order for our Maker to trade ZRX for WETH:
>>> from zero_ex.contract_wrappers.exchange.types import Order, order_to_jsdict
>>> from zero_ex.order_utils import (
... asset_data_utils,
... Order,
... order_to_jsdict,
... sign_hash)
>>> import random
>>> from datetime import datetime, timedelta
@@ -254,7 +253,7 @@ consists just of our order):
Select an order from the orderbook
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>> from zero_ex.order_utils import jsdict_to_order
>>> from zero_ex.contract_wrappers.exchange.types import jsdict_to_order
>>> order = jsdict_to_order(orderbook.bids.records[0].order)
>>> from pprint import pprint
>>> pprint(order)
@@ -322,8 +321,8 @@ book. Now let's have the taker fill it:
... )
>>> exchange.fill_order(
... order=order,
... taker_amount=order['makerAssetAmount']/2, # note the half fill
... signature=order['signature'],
... taker_asset_fill_amount=order['makerAssetAmount']/2, # note the half fill
... signature=order['signature'].replace('0x', '').encode('utf-8'),
... tx_params=TxParams(from_=taker_address)
... )
HexBytes('0x...')

View File

@@ -1,13 +1,3 @@
#!/usr/bin/env python
#!/usr/bin/env bash
"""Script to run tests against local copy of all components."""
from os import path
import subprocess
subprocess.check_call(
(
f"{path.join('.', 'cmd_pkgs_in_dep_order.py')}"
+ f" {path.join('.', 'setup.py')} test"
).split()
)
./parallel ./setup.py test