fix(order_utils.py): validate order w/json schema (#1260)

This commit is contained in:
F. Eugene Aumson 2018-11-14 12:56:31 -05:00 committed by GitHub
parent e1d64def20
commit b961cb1952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 131 additions and 61 deletions

View File

@ -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/

View File

@ -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",

View File

@ -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
==================

View File

@ -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
)

View File

@ -0,0 +1 @@
../../../../../packages/json-schemas/schemas/

View File

@ -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(

View File

@ -0,0 +1,5 @@
from typing import Any, Dict
class RefResolver: pass
def validate(instance: Any, schema: Dict, cls=None, *args, **kwargs) -> None: pass

View File

@ -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