diff options
author | F. Eugene Aumson <feuGeneA@users.noreply.github.com> | 2019-01-09 22:58:29 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-09 22:58:29 +0800 |
commit | aa5af04447dfae24731557c6beead55bd8ff99a9 (patch) | |
tree | 1ffcc631ab078c88f85e2ab2b708f5d91b731cea /python-packages/order_utils | |
parent | 5b7eff217e9c8d09d64ff8721d7a16e1df8a7c58 (diff) | |
download | dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.tar dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.tar.gz dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.tar.bz2 dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.tar.lz dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.tar.xz dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.tar.zst dexon-sol-tools-aa5af04447dfae24731557c6beead55bd8ff99a9.zip |
Python contract demo, with lots of refactoring (#1485)
* Refine Order for Web3 compat. & add conversions
Changed some of the fields in the Order class so that it can be passed
to our contracts via Web3.
Added conversion utilities so that an Order can be easily converted to
and from a JSON-compatible dict (specifically by encoding/decoding the
`bytes` fields), to facilitate validation against the JSON schema.
Also modified JSON order schema to accept integers in addition to
stringified integers.
* Fixes for json_schemas
Has-types indicator file, py.typed, was not being included in package.
Schemas were not being properly gathered into package installation.
* Add test/demo of Exchange.getOrderInfo()
* web3 bug workaround
* Fix problem packaging contract artifacts
* Move contract addresses to their own package
* Move contract artifacts to their own package
* Add scripts to install, test & lint all components
* prettierignore files in local python dev env
* Correct missing coverage analysis for sra_client
* CI cache lint: don't save, re-use from test-python
* tag hacks as hacks
* correct merge mistake
* remove local strip_0x() in favor of eth_utils
* remove json schemas from old order_utils location
* correct merge mistake
* doctest json schemas via command-line, not code
Diffstat (limited to 'python-packages/order_utils')
6 files changed, 162 insertions, 88 deletions
diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index 06533e60a..01a6c7360 100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -21,7 +21,7 @@ class TestCommandExtension(TestCommand): """Invoke pytest.""" import pytest - exit(pytest.main()) + exit(pytest.main(["--doctest-modules"])) class LintCommand(distutils.command.build_py.build_py): @@ -165,9 +165,13 @@ setup( "ganache": GanacheCommand, }, install_requires=[ + "0x-contract-addresses", + "0x-contract-artifacts", "0x-json-schemas", "eth-abi", "eth_utils", + "hypothesis>=3.31.2", # HACK! this is web3's dependency! + # above works around https://github.com/ethereum/web3.py/issues/1179 "mypy_extensions", "web3", ], @@ -189,10 +193,7 @@ setup( ] }, python_requires=">=3.6, <4", - package_data={ - "zero_ex.order_utils": ["py.typed"], - "zero_ex.contract_artifacts": ["artifacts/*"], - }, + package_data={"zero_ex.order_utils": ["py.typed"]}, package_dir={"": "src"}, license="Apache 2.0", keywords=( diff --git a/python-packages/order_utils/src/zero_ex/contract_artifacts/__init__.py b/python-packages/order_utils/src/zero_ex/contract_artifacts/__init__.py deleted file mode 100644 index ed45d2c8e..000000000 --- a/python-packages/order_utils/src/zero_ex/contract_artifacts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Solc-generated artifacts for 0x smart contracts.""" diff --git a/python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts b/python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts deleted file mode 120000 index 82d28ba87..000000000 --- a/python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts +++ /dev/null @@ -1 +0,0 @@ -../../../../../packages/contract-artifacts/artifacts
\ No newline at end of file diff --git a/python-packages/order_utils/src/zero_ex/order_utils/__init__.py b/python-packages/order_utils/src/zero_ex/order_utils/__init__.py index 24c6bfd4e..4697ad99c 100644 --- a/python-packages/order_utils/src/zero_ex/order_utils/__init__.py +++ b/python-packages/order_utils/src/zero_ex/order_utils/__init__.py @@ -10,19 +10,22 @@ just this purpose. To start it: ``docker run -d -p 8545:8545 0xorg/ganache-cli fence smart topic"``. """ +from copy import copy from enum import auto, Enum import json -from typing import Dict, Tuple +from typing import cast, Dict, NamedTuple, Tuple from pkg_resources import resource_string from mypy_extensions import TypedDict -from eth_utils import keccak, to_bytes, to_checksum_address +from eth_utils import keccak, remove_0x_prefix, to_bytes, to_checksum_address from web3 import Web3 import web3.exceptions from web3.providers.base import BaseProvider from web3.utils import datatypes +from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId +import zero_ex.contract_artifacts from zero_ex.dev_utils.type_assertions import ( assert_is_address, assert_is_hex_string, @@ -34,34 +37,6 @@ from zero_ex.json_schemas import assert_valid class _Constants: """Static data used by order utilities.""" - _contract_name_to_abi: Dict[str, Dict] = {} # class data, not instance - - @classmethod - def contract_name_to_abi(cls, contract_name: str) -> Dict: - """Return the ABI for the given contract name. - - First tries to get data from the class level storage - `_contract_name_to_abi`. If it's not there, loads it from disk, stores - it in the class data (for the next caller), and then returns it. - """ - try: - return cls._contract_name_to_abi[contract_name] - except KeyError: - cls._contract_name_to_abi[contract_name] = json.loads( - resource_string( - "zero_ex.contract_artifacts", - f"artifacts/{contract_name}.json", - ) - )["compilerOutput"]["abi"] - return cls._contract_name_to_abi[contract_name] - - network_to_exchange_addr: Dict[str, str] = { - "1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", - "3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf", - "42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2", - "50": "0x48bacb9266a570d521063ef5dd96e61686dbe788", - } - null_address = "0x0000000000000000000000000000000000000000" eip191_header = b"\x19\x01" @@ -107,47 +82,153 @@ class _Constants: class Order(TypedDict): # pylint: disable=too-many-instance-attributes - """Object representation of a 0x order.""" + """A Web3-compatible representation of the Exchange.Order struct.""" makerAddress: str takerAddress: str feeRecipientAddress: str senderAddress: str - makerAssetAmount: str - takerAssetAmount: str - makerFee: str - takerFee: str - expirationTimeSeconds: str - salt: str - makerAssetData: str - takerAssetData: str - exchangeAddress: str + makerAssetAmount: int + takerAssetAmount: int + makerFee: int + takerFee: int + expirationTimeSeconds: int + salt: int + makerAssetData: bytes + takerAssetData: bytes def make_empty_order() -> Order: """Construct an empty order. - Initializes all strings to "0x0000000000000000000000000000000000000000" - and all numbers to 0. + 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": _Constants.null_address, - "takerAssetData": _Constants.null_address, - "salt": "0", - "makerFee": "0", - "takerFee": "0", - "makerAssetAmount": "0", - "takerAssetAmount": "0", - "expirationTimeSeconds": "0", - "exchangeAddress": _Constants.null_address, + "makerAssetData": (b"\x00") * 20, + "takerAssetData": (b"\x00") * 20, + "salt": 0, + "makerFee": 0, + "takerFee": 0, + "makerAssetAmount": 0, + "takerAssetAmount": 0, + "expirationTimeSeconds": 0, } -def generate_order_hash_hex(order: Order) -> str: +def order_to_jsdict( + order: Order, exchange_address="0x0000000000000000000000000000000000000000" +) -> dict: + """Convert a Web3-compatible order struct to a JSON-schema-compatible dict. + + More specifically, do explicit decoding for the `bytes` fields. + + >>> 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 + + assert_valid(jsdict, "/orderSchema") + + return jsdict + + +def jsdict_order_to_struct(jsdict: dict) -> Order: + r"""Convert a JSON-schema-compatible dict order to a Web3-compatible struct. + + More specifically, do explicit encoding of the `bytes` fields. + + >>> import pprint + >>> pprint.pprint(jsdict_order_to_struct( + ... { + ... '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"]) + ) + + 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. :param order: The order to be hashed. Must conform to `the 0x order JSON schema <https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_schema.json>`_. @@ -167,14 +248,15 @@ def generate_order_hash_hex(order: Order) -> str: ... 'takerFee': "0", ... 'expirationTimeSeconds': "12345", ... 'salt': "12345", - ... 'makerAssetData': "0x0000000000000000000000000000000000000000", - ... 'takerAssetData': "0x0000000000000000000000000000000000000000", - ... 'exchangeAddress': "0x0000000000000000000000000000000000000000", + ... 'makerAssetData': (0).to_bytes(1, byteorder='big') * 20, + ... 'takerAssetData': (0).to_bytes(1, byteorder='big') * 20, ... }, + ... exchange_address="0x0000000000000000000000000000000000000000", ... ) '55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692' """ # noqa: E501 (line too long) - assert_valid(order, "/orderSchema") + assert_is_address(exchange_address, "exchange_address") + assert_valid(order_to_jsdict(order, exchange_address), "/orderSchema") def pad_20_bytes_to_32(twenty_bytes: bytes): return bytes(12) + twenty_bytes @@ -184,7 +266,7 @@ def generate_order_hash_hex(order: Order) -> str: eip712_domain_struct_hash = keccak( _Constants.eip712_domain_struct_header - + pad_20_bytes_to_32(to_bytes(hexstr=order["exchangeAddress"])) + + pad_20_bytes_to_32(to_bytes(hexstr=exchange_address)) ) eip712_order_struct_hash = keccak( @@ -199,8 +281,8 @@ def generate_order_hash_hex(order: Order) -> str: + int_to_32_big_endian_bytes(int(order["takerFee"])) + int_to_32_big_endian_bytes(int(order["expirationTimeSeconds"])) + int_to_32_big_endian_bytes(int(order["salt"])) - + keccak(to_bytes(hexstr=order["makerAssetData"])) - + keccak(to_bytes(hexstr=order["takerAssetData"])) + + keccak(to_bytes(hexstr=order["makerAssetData"].hex())) + + keccak(to_bytes(hexstr=order["takerAssetData"].hex())) ) return keccak( @@ -210,6 +292,14 @@ def generate_order_hash_hex(order: Order) -> str: ).hex() +class OrderInfo(NamedTuple): + """A Web3-compatible representation of the Exchange.OrderInfo struct.""" + + order_status: str + order_hash: bytes + order_taker_asset_filled_amount: int + + def is_valid_signature( provider: BaseProvider, data: str, signature: str, signer_address: str ) -> Tuple[bool, str]: @@ -241,12 +331,13 @@ def is_valid_signature( web3_instance = Web3(provider) # false positive from pylint: disable=no-member - network_id = web3_instance.net.version - contract_address = _Constants.network_to_exchange_addr[network_id] + contract_address = NETWORK_TO_ADDRESSES[ + NetworkId(int(web3_instance.net.version)) + ].exchange # false positive from pylint: disable=no-member contract: datatypes.Contract = web3_instance.eth.contract( address=to_checksum_address(contract_address), - abi=_Constants.contract_name_to_abi("Exchange"), + abi=zero_ex.contract_artifacts.abi_by_name("Exchange"), ) try: return ( diff --git a/python-packages/order_utils/test/test_doctest.py b/python-packages/order_utils/test/test_doctest.py deleted file mode 100644 index 297f75e75..000000000 --- a/python-packages/order_utils/test/test_doctest.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Exercise doctests for all of our modules.""" - -from doctest import testmod -import pkgutil -import importlib - -import zero_ex - - -def test_all_doctests(): - """Gather zero_ex.* modules and doctest them.""" - for (_, modname, _) in pkgutil.walk_packages( - path=zero_ex.__path__, prefix="zero_ex." - ): - module = importlib.import_module(modname) - print(module) - (failure_count, _) = testmod(module) - assert failure_count == 0 diff --git a/python-packages/order_utils/test/test_generate_order_hash_hex.py b/python-packages/order_utils/test/test_generate_order_hash_hex.py index 6869a40ed..38b503289 100644 --- a/python-packages/order_utils/test/test_generate_order_hash_hex.py +++ b/python-packages/order_utils/test/test_generate_order_hash_hex.py @@ -8,5 +8,7 @@ def test_get_order_hash_hex__empty_order(): expected_hash_hex = ( "faa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422" ) - actual_hash_hex = generate_order_hash_hex(make_empty_order()) + actual_hash_hex = generate_order_hash_hex( + make_empty_order(), "0x0000000000000000000000000000000000000000" + ) assert actual_hash_hex == expected_hash_hex |