Python doc polish (#1757)
* Exercise doctests as a test not as a linter * Add a contract artifact doctest, and exercise it * Clean up linter issues * Change asset data decoding output type Previously, it was a TypedDict, but that was causing problems. Sphinx seems to be broken, such that none of the fields of the class were being rendered into the doc. Thinking on it further, I decided that a NamedTuple makes more sense here anyways, since tuples are immutable and this output value isn't something someone should ever build or modify. And, NamedTuple is getting its fields properly rendered by Sphinx. * Add type annotations to JSON schemas docs * Add doc publish metadata file for middlewares pkg * Improve documentation Note that none of the changes to .py files impact functionality in any way, because the changes are restricted to "docstrings", which to the Python interpreter are simply no-op statements. However, one caveat to that is that much of these docstring changes DO affect the functionality of automated test runs, because all of the code examples (blocks beginning with `>>> `) are "doctests", which are exercised via the test framework. The index.rst files are the top-level templates for generating the documentation, and the "automodule"/"autoclass"/etc statements pull in the docstrings from the source code. * correct package name in doc URL * Move sra_client module into zero_ex namespace * Add functions to encode asset data to bytes * Fix: SRA client was deserializing orders weirdly The generated code was transforming the order structure, from the camel case field name format in the spec, into the snake case field name format expected by Python convention. With this problem in place, the only way to take an order from a relayer and send it to a contract (for fill, cancel, etc) was to manually transform the field names, one by one, into a new structure. * Fix problem with Web3/JSON order conversion utils * doctest: maker, trade ZRX for WETH, not vice versa * Remove redundant test * Construct order in native Python, not JSON Then convert it to JSON before sending it to the relayer. * doctest: simplify asset units * Add doctests for filling and cancelling * Minor doctetst copy edits; whitespace * Rename function, and add optional parameter * Tweak docstrings on JSON conversion functions. * Demo asset data decoding to view asset pairs * Demo selecting an order from the order book And have taker take it. * Rename variable * Abstract ganache from examples Doing that exposed excessive use of the verbose NETWORK_TO_ADDRESSES[NetworkId.Ganache] construct, so simplified that, which ripped into eliminating other temporary variables that had been used to hold specific contract addresses. Also cleaned up some misplaced import statements. * Add missing SRA client doc publication metadata * Ran prettier on new SRA client doc pub metadata * Remove local env customizations in doc metadata * Eliminate temporary variable * Rename variable * Show `pip install` in every package's doc * Doc NetorkID & pagination params as int, not float * Clean up unmatched parenthesis in docs
This commit is contained in:
@@ -181,6 +181,7 @@ setup(
|
||||
"black",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"deprecated",
|
||||
"mypy",
|
||||
"mypy_extensions",
|
||||
"pycodestyle",
|
||||
|
@@ -21,12 +21,14 @@ zero_ex.order_utils.asset_data_utils
|
||||
:members:
|
||||
|
||||
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
|
||||
|
||||
See source for class properties. Sphinx is having problems generating docs for ``TypedDict`` declarations; pull requests welcome.
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData
|
||||
|
||||
See source for class properties. Sphinx is having problems generating docs for ``TypedDict`` declarations; pull requests welcome.
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
@@ -10,8 +10,8 @@ from typing import Any, List
|
||||
|
||||
from mypy_extensions import TypedDict
|
||||
|
||||
from web3 import Web3
|
||||
from eth_abi import encode_abi
|
||||
from web3 import Web3
|
||||
|
||||
from .type_assertions import assert_is_string, assert_is_list
|
||||
|
||||
|
@@ -1,5 +1,12 @@
|
||||
"""Order utilities for 0x applications.
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
Install the package with pip::
|
||||
|
||||
pip install 0x-order-utils
|
||||
|
||||
Some methods require the caller to pass in a `Web3.BaseProvider`:code: object.
|
||||
For local testing one may construct such a provider pointing at an instance of
|
||||
`ganache-cli <https://www.npmjs.com/package/ganache-cli>`_ which has the 0x
|
||||
@@ -7,52 +14,51 @@ 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:.
|
||||
|
||||
Creating a 0x Order
|
||||
--------------------
|
||||
Constructing an order
|
||||
---------------------
|
||||
|
||||
Here is a short demonstration on how to create a 0x order.
|
||||
|
||||
>>> import pprint
|
||||
>>> from zero_ex.contract_addresses import (
|
||||
... NETWORK_TO_ADDRESSES, NetworkId)
|
||||
>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
|
||||
>>> from zero_ex.order_utils import asset_data_utils, Order
|
||||
>>> NULL_ADDRESS = "0x0000000000000000000000000000000000000000"
|
||||
>>> from datetime import datetime, timedelta
|
||||
>>> import random
|
||||
>>> my_address = "0x5409ed021d9299bf6814279a6a1411a7e866a631"
|
||||
>>> exchange_address = NETWORK_TO_ADDRESSES[NetworkId.MAINNET].exchange
|
||||
>>> weth_address = NETWORK_TO_ADDRESSES[NetworkId.MAINNET].ether_token
|
||||
>>> zrx_address = NETWORK_TO_ADDRESSES[NetworkId.MAINNET].zrx_token
|
||||
>>> maker_asset_data = (
|
||||
... asset_data_utils.encode_erc20_asset_data(weth_address))
|
||||
>>> taker_asset_data = (
|
||||
... asset_data_utils.encode_erc20_asset_data(zrx_address))
|
||||
>>> example_order: Order = {
|
||||
... "makerAddress": my_address,
|
||||
... "takerAddress": NULL_ADDRESS,
|
||||
... "exchangeAddress": exchange_address,
|
||||
... "senderAddress": NULL_ADDRESS,
|
||||
... "feeRecipientAddress": NULL_ADDRESS,
|
||||
... "makerAssetData": maker_asset_data,
|
||||
... "takerAssetData": taker_asset_data,
|
||||
... "salt": 123456789,
|
||||
... "makerFee": 0,
|
||||
... "takerFee": 0,
|
||||
... "makerAssetAmount": 1 * 10 ** 18, # Converting token amount to base unit with 18 decimals
|
||||
... "takerAssetAmount": 500 * 10 ** 18, # Converting token amount to base unit with 18 decimals
|
||||
... "expirationTimeSeconds": 1553553429,
|
||||
... }
|
||||
>>> 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': '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
|
||||
'expirationTimeSeconds': 1553553429,
|
||||
{'exchangeAddress': '0x...',
|
||||
'expirationTimeSeconds': ...,
|
||||
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAddress': '0x5409ed021d9299bf6814279a6a1411a7e866a631',
|
||||
'makerAddress': '0x...',
|
||||
'makerAssetAmount': 1000000000000000000,
|
||||
'makerAssetData': '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
||||
'makerAssetData': b...,
|
||||
'makerFee': 0,
|
||||
'salt': 123456789,
|
||||
'salt': ...,
|
||||
'senderAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAssetAmount': 500000000000000000000,
|
||||
'takerAssetData': '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498',
|
||||
'takerAssetData': b...,
|
||||
'takerFee': 0}
|
||||
""" # noqa E501
|
||||
|
||||
@@ -128,7 +134,29 @@ class _Constants:
|
||||
|
||||
|
||||
class Order(TypedDict): # pylint: disable=too-many-instance-attributes
|
||||
"""A Web3-compatible representation of the Exchange.Order struct."""
|
||||
"""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."""
|
||||
@@ -203,11 +231,14 @@ def make_empty_order() -> Order:
|
||||
|
||||
|
||||
def order_to_jsdict(
|
||||
order: Order, exchange_address="0x0000000000000000000000000000000000000000"
|
||||
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.
|
||||
More specifically, do explicit decoding for the `bytes`:code: fields, and
|
||||
convert numerics to strings.
|
||||
|
||||
>>> import pprint
|
||||
>>> pprint.pprint(order_to_jsdict(
|
||||
@@ -228,18 +259,18 @@ def order_to_jsdict(
|
||||
... },
|
||||
... ))
|
||||
{'exchangeAddress': '0x0000000000000000000000000000000000000000',
|
||||
'expirationTimeSeconds': 1,
|
||||
'expirationTimeSeconds': '1',
|
||||
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAssetAmount': 1,
|
||||
'makerAssetAmount': '1',
|
||||
'makerAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'makerFee': 0,
|
||||
'salt': 1,
|
||||
'makerFee': '0',
|
||||
'salt': '1',
|
||||
'senderAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAssetAmount': 1,
|
||||
'takerAssetAmount': '1',
|
||||
'takerAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'takerFee': 0}
|
||||
'takerFee': '0'}
|
||||
"""
|
||||
jsdict = cast(Dict, copy(order))
|
||||
|
||||
@@ -249,29 +280,43 @@ def order_to_jsdict(
|
||||
|
||||
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_order_to_struct(jsdict: dict) -> Order:
|
||||
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.
|
||||
More specifically, do explicit encoding of the `bytes`:code: fields, and
|
||||
parse integers from strings.
|
||||
|
||||
>>> import pprint
|
||||
>>> pprint.pprint(jsdict_order_to_struct(
|
||||
>>> 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,
|
||||
... 'makerAssetAmount': "1000000000000000000",
|
||||
... 'takerAssetAmount': "1000000000000000000",
|
||||
... 'makerFee': "0",
|
||||
... 'takerFee': "0",
|
||||
... 'expirationTimeSeconds': "12345",
|
||||
... 'salt': "12345",
|
||||
... 'makerAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
|
||||
@@ -303,6 +348,16 @@ def jsdict_order_to_struct(jsdict: dict) -> Order:
|
||||
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
|
||||
|
@@ -1,8 +1,9 @@
|
||||
"""Asset data encoding and decoding utilities."""
|
||||
|
||||
from mypy_extensions import TypedDict
|
||||
from typing import NamedTuple
|
||||
|
||||
import eth_abi
|
||||
from deprecated.sphinx import deprecated
|
||||
|
||||
from zero_ex.dev_utils import abi_utils
|
||||
from zero_ex.dev_utils.type_assertions import assert_is_string, assert_is_int
|
||||
@@ -13,23 +14,30 @@ ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53
|
||||
SELECTOR_LENGTH = 10
|
||||
|
||||
|
||||
class ERC20AssetData(TypedDict):
|
||||
class ERC20AssetData(NamedTuple):
|
||||
"""Object interface to ERC20 asset data."""
|
||||
|
||||
asset_proxy_id: str
|
||||
"""asset proxy id"""
|
||||
"""Asset proxy identifier."""
|
||||
|
||||
token_address: str
|
||||
"""Token address"""
|
||||
|
||||
|
||||
class ERC721AssetData(TypedDict):
|
||||
class ERC721AssetData(NamedTuple):
|
||||
"""Object interface to ERC721 asset data."""
|
||||
|
||||
asset_proxy_id: str
|
||||
"""Asset proxy identifier."""
|
||||
|
||||
token_address: str
|
||||
"""Token address"""
|
||||
|
||||
token_id: int
|
||||
"""Token identifier."""
|
||||
|
||||
|
||||
@deprecated(reason='use `"0x"+encode_erc20().hex()` instead')
|
||||
def encode_erc20_asset_data(token_address: str) -> str:
|
||||
"""Encode an ERC20 token address into an asset data string.
|
||||
|
||||
@@ -48,13 +56,28 @@ def encode_erc20_asset_data(token_address: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def encode_erc20(token_address: str) -> bytes:
|
||||
"""Encode an ERC20 token address into asset data bytes.
|
||||
|
||||
:param token_address: the ERC20 token's contract address.
|
||||
:returns: hex encoded asset data string, usable in the makerAssetData or
|
||||
takerAssetData fields in a 0x order.
|
||||
|
||||
>>> encode_erc20('0x1dc4c1cefef38a777b15aa20260a54e584b16c48').hex()
|
||||
'f47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48'
|
||||
"""
|
||||
assert_is_string(token_address, "token_address")
|
||||
|
||||
return abi_utils.simple_encode("ERC20Token(address)", token_address)
|
||||
|
||||
|
||||
def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
|
||||
"""Decode an ERC20 asset data hex string.
|
||||
|
||||
:param asset_data: String produced by prior call to encode_erc20_asset_data()
|
||||
|
||||
>>> decode_erc20_asset_data("0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48")
|
||||
{'asset_proxy_id': '0xf47261b0', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'}
|
||||
ERC20AssetData(asset_proxy_id='0xf47261b0', token_address='0x1dc4c1cefef38a777b15aa20260a54e584b16c48')
|
||||
""" # noqa: E501 (line too long)
|
||||
assert_is_string(asset_data, "asset_data")
|
||||
|
||||
@@ -79,9 +102,12 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
|
||||
["address"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
|
||||
)[0]
|
||||
|
||||
return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
|
||||
return ERC20AssetData(
|
||||
asset_proxy_id=asset_proxy_id, token_address=token_address
|
||||
)
|
||||
|
||||
|
||||
@deprecated(reason='use `"0x"+encode_erc721().hex()` instead')
|
||||
def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
|
||||
"""Encode an ERC721 asset data hex string.
|
||||
|
||||
@@ -104,11 +130,30 @@ def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
|
||||
)
|
||||
|
||||
|
||||
def encode_erc721(token_address: str, token_id: int) -> bytes:
|
||||
"""Encode an ERC721 token address into asset data bytes.
|
||||
|
||||
:param token_address: the ERC721 token's contract address.
|
||||
:param token_id: the identifier of the asset's instance of the token.
|
||||
:returns: hex encoded asset data string, usable in the makerAssetData or
|
||||
takerAssetData fields in a 0x order.
|
||||
|
||||
>>> encode_erc721('0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 1).hex()
|
||||
'025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001'
|
||||
""" # noqa: E501 (line too long)
|
||||
assert_is_string(token_address, "token_address")
|
||||
assert_is_int(token_id, "token_id")
|
||||
|
||||
return abi_utils.simple_encode(
|
||||
"ERC721Token(address,uint256)", token_address, token_id
|
||||
)
|
||||
|
||||
|
||||
def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData:
|
||||
"""Decode an ERC721 asset data hex string.
|
||||
|
||||
>>> decode_erc721_asset_data('0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001')
|
||||
{'asset_proxy_id': '0x02571792', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 'token_id': 1}
|
||||
ERC721AssetData(asset_proxy_id='0x02571792', token_address='0x1dc4c1cefef38a777b15aa20260a54e584b16c48', token_id=1)
|
||||
""" # noqa: E501 (line too long)
|
||||
assert_is_string(asset_data, "asset_data")
|
||||
|
||||
@@ -134,8 +179,8 @@ def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData:
|
||||
["address", "uint256"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
|
||||
)
|
||||
|
||||
return {
|
||||
"asset_proxy_id": asset_proxy_id,
|
||||
"token_address": token_address,
|
||||
"token_id": token_id,
|
||||
}
|
||||
return ERC721AssetData(
|
||||
asset_proxy_id=asset_proxy_id,
|
||||
token_address=token_address,
|
||||
token_id=token_id,
|
||||
)
|
||||
|
@@ -0,0 +1,2 @@
|
||||
def deprecated(*args, **kwargs):
|
||||
...
|
Reference in New Issue
Block a user