aboutsummaryrefslogtreecommitdiffstats
path: root/python-packages/order_utils/src
diff options
context:
space:
mode:
Diffstat (limited to 'python-packages/order_utils/src')
-rw-r--r--python-packages/order_utils/src/conf.py54
-rw-r--r--python-packages/order_utils/src/doc_static/.gitkeep0
-rw-r--r--python-packages/order_utils/src/doc_templates/.gitkeep0
-rw-r--r--python-packages/order_utils/src/index.rst33
-rw-r--r--python-packages/order_utils/src/zero_ex/__init__.py2
-rw-r--r--python-packages/order_utils/src/zero_ex/contract_artifacts/__init__.py1
l---------python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts1
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/__init__.py1
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py101
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py89
-rw-r--r--python-packages/order_utils/src/zero_ex/json_schemas/__init__.py61
l---------python-packages/order_utils/src/zero_ex/json_schemas/schemas1
-rw-r--r--python-packages/order_utils/src/zero_ex/order_utils/__init__.py405
-rw-r--r--python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py139
-rw-r--r--python-packages/order_utils/src/zero_ex/order_utils/py.typed0
15 files changed, 888 insertions, 0 deletions
diff --git a/python-packages/order_utils/src/conf.py b/python-packages/order_utils/src/conf.py
new file mode 100644
index 000000000..6b6776d01
--- /dev/null
+++ b/python-packages/order_utils/src/conf.py
@@ -0,0 +1,54 @@
+"""Configuration file for the Sphinx documentation builder."""
+
+# Reference: http://www.sphinx-doc.org/en/master/config
+
+from typing import List
+import pkg_resources
+
+
+# pylint: disable=invalid-name
+# because these variables are not named in upper case, as globals should be.
+
+project = "0x-order-utils"
+# pylint: disable=redefined-builtin
+copyright = "2018, ZeroEx, Intl."
+author = "F. Eugene Aumson"
+version = pkg_resources.get_distribution("0x-order-utils").version
+release = "" # The full version, including alpha/beta/rc tags
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.coverage",
+ "sphinx.ext.viewcode",
+]
+
+templates_path = ["doc_templates"]
+
+source_suffix = ".rst"
+# eg: source_suffix = [".rst", ".md"]
+
+master_doc = "index" # The master toctree document.
+
+language = None
+
+exclude_patterns: List[str] = []
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+html_theme = "alabaster"
+
+html_static_path = ["doc_static"]
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "order_utilspydoc"
+
+# -- Extension configuration:
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {"https://docs.python.org/": None}
diff --git a/python-packages/order_utils/src/doc_static/.gitkeep b/python-packages/order_utils/src/doc_static/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python-packages/order_utils/src/doc_static/.gitkeep
diff --git a/python-packages/order_utils/src/doc_templates/.gitkeep b/python-packages/order_utils/src/doc_templates/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python-packages/order_utils/src/doc_templates/.gitkeep
diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst
new file mode 100644
index 000000000..551487ab1
--- /dev/null
+++ b/python-packages/order_utils/src/index.rst
@@ -0,0 +1,33 @@
+.. source for the sphinx-generated build/docs/web/index.html
+
+Python zero_ex.order_utils
+==========================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+.. autoclass:: zero_ex.order_utils.Order
+ :members:
+
+See source for class properties. Sphinx does not easily generate class property docs; pull requests welcome.
+
+.. automodule:: zero_ex.order_utils
+ :members:
+
+.. automodule:: zero_ex.order_utils.asset_data_utils
+ :members:
+
+.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
+
+.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData
+
+.. automodule:: zero_ex.json_schemas
+ :members:
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/python-packages/order_utils/src/zero_ex/__init__.py b/python-packages/order_utils/src/zero_ex/__init__.py
new file mode 100644
index 000000000..e90d833db
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/__init__.py
@@ -0,0 +1,2 @@
+"""0x Python API."""
+__import__("pkg_resources").declare_namespace(__name__)
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
new file mode 100644
index 000000000..ed45d2c8e
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/contract_artifacts/__init__.py
@@ -0,0 +1 @@
+"""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
new file mode 120000
index 000000000..82d28ba87
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts
@@ -0,0 +1 @@
+../../../../../packages/contract-artifacts/artifacts \ No newline at end of file
diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py
new file mode 100644
index 000000000..b6a224d2c
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py
@@ -0,0 +1 @@
+"""Dev utils to be shared across 0x projects and packages."""
diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py
new file mode 100644
index 000000000..3fec775b0
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py
@@ -0,0 +1,101 @@
+"""Ethereum ABI utilities.
+
+Builds on the eth-abi package, adding some convenience methods like those found
+in npmjs.com/package/ethereumjs-abi. Ideally, all of this code should be
+pushed upstream into eth-abi.
+"""
+
+import re
+from typing import Any, List
+
+from mypy_extensions import TypedDict
+
+from web3 import Web3
+from eth_abi import encode_abi
+
+from .type_assertions import assert_is_string, assert_is_list
+
+
+class MethodSignature(TypedDict, total=False):
+ """Object interface to an ABI method signature."""
+
+ method: str
+ args: List[str]
+
+
+def parse_signature(signature: str) -> MethodSignature:
+ """Parse a method signature into its constituent parts.
+
+ >>> parse_signature("ERC20Token(address)")
+ {'method': 'ERC20Token', 'args': ['address']}
+ """
+ assert_is_string(signature, "signature")
+
+ matches = re.match(r"^(\w+)\((.+)\)$", signature)
+ if matches is None:
+ raise ValueError(f"Invalid method signature {signature}")
+ return {"method": matches[1], "args": matches[2].split(",")}
+
+
+def elementary_name(name: str) -> str:
+ """Convert from short to canonical names; barely implemented.
+
+ Modeled after ethereumjs-abi's ABI.elementaryName(), but only implemented
+ to support our particular use case and a few other simple ones.
+
+ >>> elementary_name("address")
+ 'address'
+ >>> elementary_name("uint")
+ 'uint256'
+ """
+ assert_is_string(name, "name")
+
+ return {
+ "int": "int256",
+ "uint": "uint256",
+ "fixed": "fixed128x128",
+ "ufixed": "ufixed128x128",
+ }.get(name, name)
+
+
+def event_id(name: str, types: List[str]) -> str:
+ """Return the Keccak-256 hash of the given method.
+
+ >>> event_id("ERC20Token", ["address"])
+ '0xf47261b06eedbfce68afd46d0f3c27c60b03faad319eaf33103611cf8f6456ad'
+ """
+ assert_is_string(name, "name")
+ assert_is_list(types, "types")
+
+ signature = f"{name}({','.join(list(map(elementary_name, types)))})"
+ return Web3.sha3(text=signature).hex()
+
+
+def method_id(name: str, types: List[str]) -> str:
+ """Return the 4-byte method identifier.
+
+ >>> method_id("ERC20Token", ["address"])
+ '0xf47261b0'
+ """
+ assert_is_string(name, "name")
+ assert_is_list(types, "types")
+
+ return event_id(name, types)[0:10]
+
+
+def simple_encode(method: str, *args: Any) -> bytes:
+ r"""Encode a method ABI.
+
+ >>> simple_encode("ERC20Token(address)", "0x1dc4c1cefef38a777b15aa20260a54e584b16c48")
+ b'\xf4ra\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\xc4\xc1\xce\xfe\xf3\x8aw{\x15\xaa &\nT\xe5\x84\xb1lH'
+ """ # noqa: E501 (line too long)
+ assert_is_string(method, "method")
+
+ signature: MethodSignature = parse_signature(method)
+
+ return bytes.fromhex(
+ (
+ method_id(signature["method"], signature["args"])
+ + encode_abi(signature["args"], args).hex()
+ )[2:]
+ )
diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py
new file mode 100644
index 000000000..1dcfb39a9
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py
@@ -0,0 +1,89 @@
+"""Assertions for runtime type checking of function arguments."""
+
+from typing import Any
+
+from eth_utils import is_address
+from web3.providers.base import BaseProvider
+
+
+def assert_is_string(value: Any, name: str) -> None:
+ """If :param value: isn't of type str, raise a TypeError.
+
+ >>> try: assert_is_string(123, 'var')
+ ... except TypeError as type_error: print(str(type_error))
+ ...
+ expected variable 'var', with value 123, to have type 'str', not 'int'
+ """
+ if not isinstance(value, str):
+ raise TypeError(
+ f"expected variable '{name}', with value {str(value)}, to have"
+ + f" type 'str', not '{type(value).__name__}'"
+ )
+
+
+def assert_is_list(value: Any, name: str) -> None:
+ """If :param value: isn't of type list, raise a TypeError.
+
+ >>> try: assert_is_list(123, 'var')
+ ... except TypeError as type_error: print(str(type_error))
+ ...
+ expected variable 'var', with value 123, to have type 'list', not 'int'
+ """
+ if not isinstance(value, list):
+ raise TypeError(
+ f"expected variable '{name}', with value {str(value)}, to have"
+ + f" type 'list', not '{type(value).__name__}'"
+ )
+
+
+def assert_is_int(value: Any, name: str) -> None:
+ """If :param value: isn't of type int, raise a TypeError.
+
+ >>> try: assert_is_int('asdf', 'var')
+ ... except TypeError as type_error: print(str(type_error))
+ ...
+ expected variable 'var', with value asdf, to have type 'int', not 'str'
+ """
+ if not isinstance(value, int):
+ raise TypeError(
+ f"expected variable '{name}', with value {str(value)}, to have"
+ + f" type 'int', not '{type(value).__name__}'"
+ )
+
+
+def assert_is_hex_string(value: Any, name: str) -> None:
+ """Assert that :param value: is a string of hex chars.
+
+ If :param value: isn't a str, raise a TypeError. If it is a string but
+ contains non-hex characters ("0x" prefix permitted), raise a ValueError.
+ """
+ assert_is_string(value, name)
+ int(value, 16) # raises a ValueError if value isn't a base-16 str
+
+
+def assert_is_address(value: Any, name: str) -> None:
+ """Assert that `value` is a valid Ethereum address.
+
+ If `value` isn't a hex string, raise a TypeError. If it isn't a valid
+ Ethereum address, raise a ValueError.
+ """
+ assert_is_hex_string(value, name)
+ if not is_address(value):
+ raise ValueError(
+ f"Expected variable '{name}' to be a valid Ethereum"
+ + " address, but it's not."
+ )
+
+
+def assert_is_provider(value: Any, name: str) -> None:
+ """Assert that `value` is a Web3 provider.
+
+ If `value` isn't a Web3 provider, raise a TypeError.
+ """
+ # TODO: make this provider check more flexible.
+ # https://app.asana.com/0/684263176955174/901300863045491/f
+ if not isinstance(value, BaseProvider):
+ raise TypeError(
+ f"Expected variable '{name}' to be an instance of a Web3 provider,"
+ + " but it's not."
+ )
diff --git a/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py b/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py
new file mode 100644
index 000000000..2a1728b8a
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py
@@ -0,0 +1,61 @@
+"""JSON schemas and associated utilities."""
+
+from os import path
+import json
+from typing import Mapping
+
+from pkg_resources import resource_string
+import jsonschema
+
+
+def assert_valid(data: Mapping, schema_id: str) -> None:
+ """Validate the given `data` against the specified `schema`.
+
+ :param data: Python dictionary to be validated as a JSON object.
+ :param schema_id: id property of the JSON schema to validate against. Must
+ be one of those listed in `the 0x JSON schema files
+ <https://github.com/0xProject/0x-monorepo/tree/development/packages/json-schemas/schemas>`_.
+
+ Raises an exception if validation fails.
+
+ >>> assert_valid(
+ ... {'v': 27, 'r': '0x'+'f'*64, 's': '0x'+'f'*64},
+ ... '/ECSignature',
+ ... )
+ """
+ # noqa
+ class LocalRefResolver(jsonschema.RefResolver):
+ """Resolve package-local JSON schema id's."""
+
+ def __init__(self):
+ self.ref_to_file = {
+ "/addressSchema": "address_schema.json",
+ "/hexSchema": "hex_schema.json",
+ "/orderSchema": "order_schema.json",
+ "/wholeNumberSchema": "whole_number_schema.json",
+ "/ECSignature": "ec_signature_schema.json",
+ "/ecSignatureParameterSchema": (
+ "ec_signature_parameter_schema.json" + ""
+ ),
+ }
+ jsonschema.RefResolver.__init__(self, "", "")
+
+ def resolve_from_url(self, url):
+ """Resolve the given URL."""
+ ref = url.replace("file://", "")
+ if ref in self.ref_to_file:
+ return json.loads(
+ resource_string(
+ "zero_ex.json_schemas",
+ f"schemas/{self.ref_to_file[ref]}",
+ )
+ )
+ raise jsonschema.ValidationError(
+ f"Unknown ref '{ref}'. "
+ + f"Known refs: {list(self.ref_to_file.keys())}."
+ )
+
+ resolver = LocalRefResolver()
+ jsonschema.validate(
+ data, resolver.resolve_from_url(schema_id), resolver=resolver
+ )
diff --git a/python-packages/order_utils/src/zero_ex/json_schemas/schemas b/python-packages/order_utils/src/zero_ex/json_schemas/schemas
new file mode 120000
index 000000000..b8257372c
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/json_schemas/schemas
@@ -0,0 +1 @@
+../../../../../packages/json-schemas/schemas/ \ 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
new file mode 100644
index 000000000..24c6bfd4e
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/order_utils/__init__.py
@@ -0,0 +1,405 @@
+"""Order utilities for 0x applications.
+
+Some methods require the caller to pass in a `Web3.HTTPProvider` 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
+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
+--gasLimit 10000000 --db /snapshot --noVMErrorsOnRPCResponse -p 8545
+--networkId 50 -m "concert load couple harbor equip island argue ramp clarify
+fence smart topic"``.
+"""
+
+from enum import auto, Enum
+import json
+from typing import Dict, Tuple
+from pkg_resources import resource_string
+
+from mypy_extensions import TypedDict
+
+from eth_utils import keccak, 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.dev_utils.type_assertions import (
+ assert_is_address,
+ assert_is_hex_string,
+ assert_is_provider,
+)
+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"
+
+ eip712_domain_separator_schema_hash = keccak(
+ b"EIP712Domain(string name,string version,address verifyingContract)"
+ )
+
+ eip712_domain_struct_header = (
+ eip712_domain_separator_schema_hash
+ + keccak(b"0x Protocol")
+ + keccak(b"2")
+ )
+
+ eip712_order_schema_hash = keccak(
+ b"Order("
+ + b"address makerAddress,"
+ + b"address takerAddress,"
+ + b"address feeRecipientAddress,"
+ + b"address senderAddress,"
+ + b"uint256 makerAssetAmount,"
+ + b"uint256 takerAssetAmount,"
+ + b"uint256 makerFee,"
+ + b"uint256 takerFee,"
+ + b"uint256 expirationTimeSeconds,"
+ + b"uint256 salt,"
+ + b"bytes makerAssetData,"
+ + b"bytes takerAssetData"
+ + b")"
+ )
+
+ class SignatureType(Enum):
+ """Enumeration of known signature types."""
+
+ ILLEGAL = 0
+ INVALID = auto()
+ EIP712 = auto()
+ ETH_SIGN = auto()
+ WALLET = auto()
+ VALIDATOR = auto()
+ PRE_SIGNED = auto()
+ N_SIGNATURE_TYPES = auto()
+
+
+class Order(TypedDict): # pylint: disable=too-many-instance-attributes
+ """Object representation of a 0x order."""
+
+ 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
+
+
+def make_empty_order() -> Order:
+ """Construct an empty order.
+
+ Initializes all strings to "0x0000000000000000000000000000000000000000"
+ and all numbers to 0.
+ """
+ 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,
+ }
+
+
+def generate_order_hash_hex(order: Order) -> 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>`_.
+ :param exchange_address: The address to which the 0x Exchange smart
+ contract has been deployed.
+ :rtype: 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': "0x0000000000000000000000000000000000000000",
+ ... 'takerAssetData': "0x0000000000000000000000000000000000000000",
+ ... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
+ ... },
+ ... )
+ '55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692'
+ """ # noqa: E501 (line too long)
+ assert_valid(order, "/orderSchema")
+
+ def pad_20_bytes_to_32(twenty_bytes: bytes):
+ return bytes(12) + twenty_bytes
+
+ def int_to_32_big_endian_bytes(i: int):
+ return i.to_bytes(32, byteorder="big")
+
+ eip712_domain_struct_hash = keccak(
+ _Constants.eip712_domain_struct_header
+ + pad_20_bytes_to_32(to_bytes(hexstr=order["exchangeAddress"]))
+ )
+
+ eip712_order_struct_hash = keccak(
+ _Constants.eip712_order_schema_hash
+ + pad_20_bytes_to_32(to_bytes(hexstr=order["makerAddress"]))
+ + pad_20_bytes_to_32(to_bytes(hexstr=order["takerAddress"]))
+ + pad_20_bytes_to_32(to_bytes(hexstr=order["feeRecipientAddress"]))
+ + pad_20_bytes_to_32(to_bytes(hexstr=order["senderAddress"]))
+ + int_to_32_big_endian_bytes(int(order["makerAssetAmount"]))
+ + int_to_32_big_endian_bytes(int(order["takerAssetAmount"]))
+ + int_to_32_big_endian_bytes(int(order["makerFee"]))
+ + 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"]))
+ )
+
+ return keccak(
+ _Constants.eip191_header
+ + eip712_domain_struct_hash
+ + eip712_order_struct_hash
+ ).hex()
+
+
+def is_valid_signature(
+ provider: BaseProvider, data: str, signature: str, signer_address: str
+) -> Tuple[bool, str]:
+ """Check the validity of the supplied signature.
+
+ Check if the supplied ``signature`` corresponds to signing ``data`` with
+ the private key corresponding to ``signer_address``.
+
+ :param provider: A Web3 provider able to access the 0x Exchange contract.
+ :param data: The hex encoded data signed by the supplied signature.
+ :param signature: The hex encoded signature.
+ :param signer_address: The hex encoded address that signed the data to
+ produce the supplied signature.
+ :rtype: Tuple consisting of a boolean and a string. Boolean is true if
+ valid, false otherwise. If false, the string describes the reason.
+
+ >>> is_valid_signature(
+ ... Web3.HTTPProvider("http://127.0.0.1:8545"),
+ ... '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0',
+ ... '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403',
+ ... '0x5409ed021d9299bf6814279a6a1411a7e866a631',
+ ... )
+ (True, '')
+ """ # noqa: E501 (line too long)
+ assert_is_provider(provider, "provider")
+ assert_is_hex_string(data, "data")
+ assert_is_hex_string(signature, "signature")
+ assert_is_address(signer_address, "signer_address")
+
+ 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]
+ # 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"),
+ )
+ try:
+ return (
+ contract.call().isValidSignature(
+ data, to_checksum_address(signer_address), signature
+ ),
+ "",
+ )
+ except web3.exceptions.BadFunctionCallOutput as exception:
+ known_revert_reasons = [
+ "LENGTH_GREATER_THAN_0_REQUIRED",
+ "SIGNATURE_ILLEGAL",
+ "SIGNATURE_UNSUPPORTED",
+ "LENGTH_0_REQUIRED",
+ "LENGTH_65_REQUIRED",
+ ]
+ for known_revert_reason in known_revert_reasons:
+ if known_revert_reason in str(exception):
+ return (False, known_revert_reason)
+ return (False, f"Unknown: {exception}")
+
+
+class ECSignature(TypedDict):
+ """Object representation of an elliptic curve signature's parameters."""
+
+ v: int
+ r: str
+ s: str
+
+
+def _parse_signature_hex_as_vrs(signature_hex: str) -> ECSignature:
+ """Parse signature hex as a concatentation of EC parameters ordered V, R, S.
+
+ >>> _parse_signature_hex_as_vrs('0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03')
+ {'v': 27, 'r': '117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113', 's': '7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b'}
+ """ # noqa: E501 (line too long)
+ signature: ECSignature = {
+ "v": int(signature_hex[2:4], 16),
+ "r": signature_hex[4:68],
+ "s": signature_hex[68:132],
+ }
+ if signature["v"] == 0 or signature["v"] == 1:
+ signature["v"] = signature["v"] + 27
+ return signature
+
+
+def _parse_signature_hex_as_rsv(signature_hex: str) -> ECSignature:
+ """Parse signature hex as a concatentation of EC parameters ordered R, S, V.
+
+ >>> _parse_signature_hex_as_rsv('0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b00')
+ {'r': '117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113', 's': '7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b', 'v': 27}
+ """ # noqa: E501 (line too long)
+ signature: ECSignature = {
+ "r": signature_hex[2:66],
+ "s": signature_hex[66:130],
+ "v": int(signature_hex[130:132], 16),
+ }
+ if signature["v"] == 0 or signature["v"] == 1:
+ signature["v"] = signature["v"] + 27
+ return signature
+
+
+def _convert_ec_signature_to_vrs_hex(signature: ECSignature) -> str:
+ """Convert elliptic curve signature object to hex hash string.
+
+ >>> _convert_ec_signature_to_vrs_hex(
+ ... {
+ ... 'r': '117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113',
+ ... 's': '7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b',
+ ... 'v': 27
+ ... }
+ ... )
+ '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b'
+ """ # noqa: E501 (line too long)
+ return (
+ "0x"
+ + signature["v"].to_bytes(1, byteorder="big").hex()
+ + signature["r"]
+ + signature["s"]
+ )
+
+
+def sign_hash(
+ provider: BaseProvider, signer_address: str, hash_hex: str
+) -> str:
+ """Sign a message with the given hash, and return the signature.
+
+ :param provider: A Web3 provider.
+ :param signer_address: The address of the signing account.
+ :param hash_hex: A hex string representing the hash, like that returned
+ from `generate_order_hash_hex()`.
+ :rtype: A string, of ASCII hex digits, representing the signature.
+
+ >>> provider = Web3.HTTPProvider("http://127.0.0.1:8545")
+ >>> sign_hash(
+ ... provider,
+ ... Web3(provider).personal.listAccounts[0],
+ ... '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004',
+ ... )
+ '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03'
+ """ # noqa: E501 (line too long)
+ assert_is_provider(provider, "provider")
+ assert_is_address(signer_address, "signer_address")
+ assert_is_hex_string(hash_hex, "hash_hex")
+
+ web3_instance = Web3(provider)
+ # false positive from pylint: disable=no-member
+ signature = web3_instance.eth.sign( # type: ignore
+ signer_address, hexstr=hash_hex.replace("0x", "")
+ ).hex()
+
+ valid_v_param_values = [27, 28]
+
+ # HACK: There is no consensus on whether the signatureHex string should be
+ # formatted as v + r + s OR r + s + v, and different clients (even
+ # different versions of the same client) return the signature params in
+ # different orders. In order to support all client implementations, we
+ # parse the signature in both ways, and evaluate if either one is a valid
+ # signature. r + s + v is the most prevalent format from eth_sign, so we
+ # attempt this first.
+
+ ec_signature = _parse_signature_hex_as_rsv(signature)
+ if ec_signature["v"] in valid_v_param_values:
+ signature_as_vrst_hex = (
+ _convert_ec_signature_to_vrs_hex(ec_signature)
+ + _Constants.SignatureType.ETH_SIGN.value.to_bytes(
+ 1, byteorder="big"
+ ).hex()
+ )
+
+ (valid, _) = is_valid_signature(
+ provider, hash_hex, signature_as_vrst_hex, signer_address
+ )
+
+ if valid is True:
+ return signature_as_vrst_hex
+
+ ec_signature = _parse_signature_hex_as_vrs(signature)
+ if ec_signature["v"] in valid_v_param_values:
+ signature_as_vrst_hex = (
+ _convert_ec_signature_to_vrs_hex(ec_signature)
+ + _Constants.SignatureType.ETH_SIGN.value.to_bytes(
+ 1, byteorder="big"
+ ).hex()
+ )
+ (valid, _) = is_valid_signature(
+ provider, hash_hex, signature_as_vrst_hex, signer_address
+ )
+
+ if valid is True:
+ return signature_as_vrst_hex
+
+ raise RuntimeError(
+ "Signature returned from web3 provider is in an unknown format."
+ + " Attempted to parse as RSV and as VRS."
+ )
diff --git a/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
new file mode 100644
index 000000000..fab7479d2
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
@@ -0,0 +1,139 @@
+"""Asset data encoding and decoding utilities."""
+
+from mypy_extensions import TypedDict
+
+import eth_abi
+
+from zero_ex.dev_utils import abi_utils
+from zero_ex.dev_utils.type_assertions import assert_is_string, assert_is_int
+
+
+ERC20_ASSET_DATA_BYTE_LENGTH = 36
+ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53
+SELECTOR_LENGTH = 10
+
+
+class ERC20AssetData(TypedDict):
+ """Object interface to ERC20 asset data."""
+
+ asset_proxy_id: str
+ token_address: str
+
+
+class ERC721AssetData(TypedDict):
+ """Object interface to ERC721 asset data."""
+
+ asset_proxy_id: str
+ token_address: str
+ token_id: int
+
+
+def encode_erc20_asset_data(token_address: str) -> str:
+ """Encode an ERC20 token address into an asset data string.
+
+ :param token_address: the ERC20 token's contract address.
+ :rtype: hex encoded asset data string, usable in the makerAssetData or
+ takerAssetData fields in a 0x order.
+
+ >>> encode_erc20_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48')
+ '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48'
+ """
+ assert_is_string(token_address, "token_address")
+
+ return (
+ "0x"
+ + abi_utils.simple_encode("ERC20Token(address)", token_address).hex()
+ )
+
+
+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'}
+ """ # noqa: E501 (line too long)
+ assert_is_string(asset_data, "asset_data")
+
+ if len(asset_data) < ERC20_ASSET_DATA_BYTE_LENGTH:
+ raise ValueError(
+ "Could not decode ERC20 Proxy Data. Expected length of encoded"
+ + f" data to be at least {str(ERC20_ASSET_DATA_BYTE_LENGTH)}."
+ + f" Got {str(len(asset_data))}."
+ )
+
+ asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH]
+ if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]):
+ raise ValueError(
+ "Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be"
+ + f" ERC20 ({abi_utils.method_id('ERC20Token', ['address'])})"
+ + f" but got {asset_proxy_id}."
+ )
+
+ # workaround for https://github.com/PyCQA/pylint/issues/1498
+ # pylint: disable=unsubscriptable-object
+ token_address = eth_abi.decode_abi(
+ ["address"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
+ )[0]
+
+ return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
+
+
+def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
+ """Encode an ERC721 asset data hex string.
+
+ :param token_address: the ERC721 token's contract address.
+ :param token_id: the identifier of the asset's instance of the token.
+ :rtype: hex encoded asset data string, usable in the makerAssetData or
+ takerAssetData fields in a 0x order.
+
+ >>> encode_erc721_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 1)
+ '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001'
+ """ # noqa: E501 (line too long)
+ assert_is_string(token_address, "token_address")
+ assert_is_int(token_id, "token_id")
+
+ return (
+ "0x"
+ + abi_utils.simple_encode(
+ "ERC721Token(address,uint256)", token_address, token_id
+ ).hex()
+ )
+
+
+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}
+ """ # noqa: E501 (line too long)
+ assert_is_string(asset_data, "asset_data")
+
+ if len(asset_data) < ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH:
+ raise ValueError(
+ "Could not decode ERC721 Asset Data. Expected length of encoded"
+ + f"data to be at least {ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH}. "
+ + f"Got {len(asset_data)}."
+ )
+
+ asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH]
+ if asset_proxy_id != abi_utils.method_id(
+ "ERC721Token", ["address", "uint256"]
+ ):
+ raise ValueError(
+ "Could not decode ERC721 Asset Data. Expected Asset Proxy Id to be"
+ + f" ERC721 ("
+ + f"{abi_utils.method_id('ERC721Token', ['address', 'uint256'])}"
+ + f"), but got {asset_proxy_id}"
+ )
+
+ (token_address, token_id) = eth_abi.decode_abi(
+ ["address", "uint256"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
+ )
+
+ return {
+ "asset_proxy_id": asset_proxy_id,
+ "token_address": token_address,
+ "token_id": token_id,
+ }
diff --git a/python-packages/order_utils/src/zero_ex/order_utils/py.typed b/python-packages/order_utils/src/zero_ex/order_utils/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/order_utils/py.typed