diff options
9 files changed, 131 insertions, 61 deletions
diff --git a/.prettierignore b/.prettierignore index 7ef0f6735..db389bdb9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ lib /packages/contract-artifacts/artifacts /python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts /packages/json-schemas/schemas +/python-packages/order_utils/src/zero_ex/json_schemas/schemas /packages/metacoin/src/contract_wrappers /packages/metacoin/artifacts /packages/sra-spec/public/ diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index 5a8db2b64..679bfb4b2 100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -159,7 +159,7 @@ setup( install_requires=[ "eth-abi", "eth_utils", - "ethereum", + "jsonschema", "mypy_extensions", "web3", ], @@ -184,6 +184,7 @@ setup( package_data={ "zero_ex.order_utils": ["py.typed"], "zero_ex.contract_artifacts": ["artifacts/*"], + "zero_ex.json_schemas": ["schemas/*"], }, package_dir={"": "src"}, license="Apache 2.0", diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst index 4d27a4b17..551487ab1 100644 --- a/python-packages/order_utils/src/index.rst +++ b/python-packages/order_utils/src/index.rst @@ -22,6 +22,9 @@ See source for class properties. Sphinx does not easily generate class property .. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData +.. automodule:: zero_ex.json_schemas + :members: + Indices and tables ================== 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 index c736d3567..a86128e0e 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 @@ -28,6 +28,7 @@ from zero_ex.dev_utils.type_assertions import ( assert_is_hex_string, assert_is_provider, ) +from zero_ex.json_schemas import assert_valid class _Constants: @@ -95,18 +96,19 @@ class _Constants: class Order(TypedDict): # pylint: disable=too-many-instance-attributes """Object representation of a 0x order.""" - maker_address: str - taker_address: str - fee_recipient_address: str - sender_address: str - maker_asset_amount: int - taker_asset_amount: int - maker_fee: int - taker_fee: int - expiration_time_seconds: int - salt: int - maker_asset_data: str - taker_asset_data: str + 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: @@ -116,22 +118,23 @@ def make_empty_order() -> Order: and all numbers to 0. """ return { - "maker_address": _Constants.null_address, - "taker_address": _Constants.null_address, - "sender_address": _Constants.null_address, - "fee_recipient_address": _Constants.null_address, - "maker_asset_data": _Constants.null_address, - "taker_asset_data": _Constants.null_address, - "salt": 0, - "maker_fee": 0, - "taker_fee": 0, - "maker_asset_amount": 0, - "taker_asset_amount": 0, - "expiration_time_seconds": 0, + "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, exchange_address: str) -> str: +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>`_. @@ -141,24 +144,25 @@ def generate_order_hash_hex(order: Order, exchange_address: str) -> str: >>> generate_order_hash_hex( ... { - ... 'maker_address': "0x0000000000000000000000000000000000000000", - ... 'taker_address': "0x0000000000000000000000000000000000000000", - ... 'fee_recipient_address': "0x0000000000000000000000000000000000000000", - ... 'sender_address': "0x0000000000000000000000000000000000000000", - ... 'maker_asset_amount': 1000000000000000000, - ... 'taker_asset_amount': 1000000000000000000, - ... 'maker_fee': 0, - ... 'taker_fee': 0, - ... 'expiration_time_seconds': 12345, - ... 'salt': 12345, - ... 'maker_asset_data': "0000000000000000000000000000000000000000", - ... 'taker_asset_data': "0000000000000000000000000000000000000000", + ... '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", ... }, - ... exchange_address="0x0000000000000000000000000000000000000000", ... ) '55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692' """ # noqa: E501 (line too long) - # TODO: use JSON schema validation to validate order. pylint: disable=fixme + assert_valid(order, "/orderSchema") + def pad_20_bytes_to_32(twenty_bytes: bytes): return bytes(12) + twenty_bytes @@ -167,23 +171,23 @@ def generate_order_hash_hex(order: Order, exchange_address: str) -> str: eip712_domain_struct_hash = keccak( _Constants.eip712_domain_struct_header - + pad_20_bytes_to_32(to_bytes(hexstr=exchange_address)) + + 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["maker_address"])) - + pad_20_bytes_to_32(to_bytes(hexstr=order["taker_address"])) - + pad_20_bytes_to_32(to_bytes(hexstr=order["fee_recipient_address"])) - + pad_20_bytes_to_32(to_bytes(hexstr=order["sender_address"])) - + int_to_32_big_endian_bytes(order["maker_asset_amount"]) - + int_to_32_big_endian_bytes(order["taker_asset_amount"]) - + int_to_32_big_endian_bytes(order["maker_fee"]) - + int_to_32_big_endian_bytes(order["taker_fee"]) - + int_to_32_big_endian_bytes(order["expiration_time_seconds"]) - + int_to_32_big_endian_bytes(order["salt"]) - + keccak(to_bytes(hexstr=order["maker_asset_data"])) - + keccak(to_bytes(hexstr=order["taker_asset_data"])) + + 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( diff --git a/python-packages/order_utils/stubs/jsonschema/__init__.pyi b/python-packages/order_utils/stubs/jsonschema/__init__.pyi new file mode 100644 index 000000000..762b58b22 --- /dev/null +++ b/python-packages/order_utils/stubs/jsonschema/__init__.pyi @@ -0,0 +1,5 @@ +from typing import Any, Dict + +class RefResolver: pass + +def validate(instance: Any, schema: Dict, cls=None, *args, **kwargs) -> None: pass diff --git a/python-packages/order_utils/stubs/jsonschema/exceptions.pyi b/python-packages/order_utils/stubs/jsonschema/exceptions.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/jsonschema/exceptions.pyi 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 af78d208c..6869a40ed 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 @@ -1,10 +1,6 @@ """Test zero_ex.order_utils.get_order_hash_hex().""" -from zero_ex.order_utils import ( - generate_order_hash_hex, - make_empty_order, - _Constants, -) +from zero_ex.order_utils import generate_order_hash_hex, make_empty_order def test_get_order_hash_hex__empty_order(): @@ -12,7 +8,5 @@ def test_get_order_hash_hex__empty_order(): expected_hash_hex = ( "faa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422" ) - actual_hash_hex = generate_order_hash_hex( - make_empty_order(), _Constants.null_address - ) + actual_hash_hex = generate_order_hash_hex(make_empty_order()) assert actual_hash_hex == expected_hash_hex |