From 7b4f63a39ca4b4e05123d2b6871c6e01f8a132a2 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 13 Nov 2018 16:30:12 -0500 Subject: feat(order_utils.py) generate_order_hash_hex() (#1234) --- python-packages/order_utils/setup.py | 8 +- .../order_utils/src/zero_ex/dev_utils/abi_utils.py | 2 +- .../src/zero_ex/order_utils/__init__.py | 154 +++++++++++++++++++++ .../src/zero_ex/order_utils/signature_utils.py | 23 +-- .../order_utils/stubs/sha3/__init__.pyi | 0 .../test/test_generate_order_hash_hex.py | 18 +++ 6 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 python-packages/order_utils/stubs/sha3/__init__.pyi create mode 100644 python-packages/order_utils/test/test_generate_order_hash_hex.py (limited to 'python-packages') diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index 1b07b612c..7f1da2f34 100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -160,7 +160,13 @@ setup( "publish": PublishCommand, "ganache": GanacheCommand, }, - install_requires=["eth-abi", "eth_utils", "mypy_extensions", "web3"], + install_requires=[ + "eth-abi", + "eth_utils", + "ethereum", + "mypy_extensions", + "web3", + ], extras_require={ "dev": [ "bandit", 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 index 71b6128ca..9afeacfdf 100644 --- 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 @@ -10,8 +10,8 @@ from typing import Any, List from mypy_extensions import TypedDict -from eth_abi import encode_abi from web3 import Web3 +from eth_abi import encode_abi from .type_assertions import assert_is_string, assert_is_list 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 80445cb6e..fb5bc2f5d 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 @@ -9,3 +9,157 @@ just this purpose. To start it: ``docker run -d -p 8545:8545 0xorg/ganache-cli --networkId 50 -m "concert load couple harbor equip island argue ramp clarify fence smart topic"``. """ + +import json +from typing import Dict +from pkg_resources import resource_string + +from mypy_extensions import TypedDict + +from eth_utils import is_address, keccak, to_checksum_address, to_bytes +from web3 import Web3 +from web3.utils import datatypes +import web3.exceptions + + +class Constants: # pylint: disable=too-few-public-methods + """Static data used by order utilities.""" + + contract_name_to_abi = { + "Exchange": json.loads( + resource_string( + "zero_ex.contract_artifacts", "artifacts/Exchange.json" + ) + )["compilerOutput"]["abi"] + } + + 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 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 + + +def make_empty_order() -> Order: + """Construct an empty order.""" + 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, + } + + +def generate_order_hash_hex(order: Order, exchange_address: str) -> str: + # docstring considered all one line by pylint: disable=line-too-long + """Calculate the hash of the given order as a hexadecimal string. + + >>> 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", + ... }, + ... exchange_address="0x0000000000000000000000000000000000000000", + ... ) + '55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692' + """ # noqa: E501 (line too long) + # TODO: use JSON schema validation to validate order. pylint: disable=fixme + 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=exchange_address)) + ) + + 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"])) + ) + + return keccak( + Constants.eip191_header + + eip712_domain_struct_hash + + eip712_order_struct_hash + ).hex() diff --git a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py index 12525ba88..2e75be6d5 100644 --- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py +++ b/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py @@ -1,30 +1,16 @@ """Signature utilities.""" -from typing import Dict, Tuple -import json -from pkg_resources import resource_string +from typing import Tuple from eth_utils import is_address, to_checksum_address from web3 import Web3 import web3.exceptions from web3.utils import datatypes +from zero_ex.order_utils import Constants from zero_ex.dev_utils.type_assertions import assert_is_hex_string -# prefer `black` formatting. pylint: disable=C0330 -EXCHANGE_ABI = json.loads( - resource_string("zero_ex.contract_artifacts", "artifacts/Exchange.json") -)["compilerOutput"]["abi"] - -network_to_exchange_addr: Dict[str, str] = { - "1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", - "3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf", - "42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2", - "50": "0x48bacb9266a570d521063ef5dd96e61686dbe788", -} - - # prefer `black` formatting. pylint: disable=C0330 def is_valid_signature( provider: Web3.HTTPProvider, data: str, signature: str, signer_address: str @@ -63,10 +49,11 @@ def is_valid_signature( web3_instance = Web3(provider) # false positive from pylint: disable=no-member network_id = web3_instance.net.version - contract_address = network_to_exchange_addr[network_id] + 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=EXCHANGE_ABI + address=to_checksum_address(contract_address), + abi=Constants.contract_name_to_abi["Exchange"], ) try: return ( diff --git a/python-packages/order_utils/stubs/sha3/__init__.pyi b/python-packages/order_utils/stubs/sha3/__init__.pyi new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e393f38d7 --- /dev/null +++ b/python-packages/order_utils/test/test_generate_order_hash_hex.py @@ -0,0 +1,18 @@ +"""Test zero_ex.order_utils.get_order_hash_hex().""" + +from zero_ex.order_utils import ( + generate_order_hash_hex, + make_empty_order, + Constants, +) + + +def test_get_order_hash_hex__empty_order(): + """Test the hashing of an uninitialized order.""" + expected_hash_hex = ( + "faa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422" + ) + actual_hash_hex = generate_order_hash_hex( + make_empty_order(), Constants.null_address + ) + assert actual_hash_hex == expected_hash_hex -- cgit v1.2.3 From e1d64def2017ced0aba599b989ad42a51fdd46fe Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 14 Nov 2018 10:41:52 -0500 Subject: feat(order_utils.py): sign_hash() (#1254) Also moved is_valid_signature() into main package module, for simplicity. Also consolidated a handul of in-line pylint disable directives into the .pylintrc config file. --- python-packages/order_utils/.pylintrc | 3 + python-packages/order_utils/setup.py | 4 - python-packages/order_utils/src/index.rst | 10 +- .../order_utils/src/zero_ex/dev_utils/abi_utils.py | 1 - .../src/zero_ex/dev_utils/type_assertions.py | 31 +++ .../src/zero_ex/order_utils/__init__.py | 253 +++++++++++++++++++-- .../src/zero_ex/order_utils/asset_data_utils.py | 4 - .../src/zero_ex/order_utils/signature_utils.py | 75 ------ .../order_utils/stubs/web3/__init__.pyi | 6 +- .../order_utils/stubs/web3/providers/__init__.pyi | 0 .../order_utils/stubs/web3/providers/base.pyi | 2 + python-packages/order_utils/test/test_doctest.py | 1 - .../test/test_generate_order_hash_hex.py | 4 +- .../order_utils/test/test_signature_utils.py | 2 +- 14 files changed, 286 insertions(+), 110 deletions(-) create mode 100644 python-packages/order_utils/.pylintrc delete mode 100644 python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py create mode 100644 python-packages/order_utils/stubs/web3/providers/__init__.pyi create mode 100644 python-packages/order_utils/stubs/web3/providers/base.pyi (limited to 'python-packages') diff --git a/python-packages/order_utils/.pylintrc b/python-packages/order_utils/.pylintrc new file mode 100644 index 000000000..937bc6313 --- /dev/null +++ b/python-packages/order_utils/.pylintrc @@ -0,0 +1,3 @@ +[MESSAGES CONTROL] +disable=C0330,line-too-long,fixme,too-few-public-methods,too-many-ancestors +# C0330 is "bad hanging indent". we use indents per `black`. diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index 7f1da2f34..5a8db2b64 100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -24,7 +24,6 @@ class TestCommandExtension(TestCommand): exit(pytest.main()) -# pylint: disable=too-many-ancestors class LintCommand(distutils.command.build_py.build_py): """Custom setuptools command class for running linters.""" @@ -90,7 +89,6 @@ class CleanCommandExtension(clean): rmtree("src/0x_order_utils.egg-info", ignore_errors=True) -# pylint: disable=too-many-ancestors class TestPublishCommand(distutils.command.build_py.build_py): """Custom command to publish to test.pypi.org.""" @@ -108,7 +106,6 @@ class TestPublishCommand(distutils.command.build_py.build_py): ) -# pylint: disable=too-many-ancestors class PublishCommand(distutils.command.build_py.build_py): """Custom command to publish to pypi.org.""" @@ -119,7 +116,6 @@ class PublishCommand(distutils.command.build_py.build_py): subprocess.check_call("twine upload dist/*".split()) # nosec -# pylint: disable=too-many-ancestors class GanacheCommand(distutils.command.build_py.build_py): """Custom command to publish to pypi.org.""" diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst index b99addabd..4d27a4b17 100644 --- a/python-packages/order_utils/src/index.rst +++ b/python-packages/order_utils/src/index.rst @@ -7,6 +7,11 @@ Python zero_ex.order_utils :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: @@ -17,11 +22,6 @@ Python zero_ex.order_utils .. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData -See source for class properties. Sphinx does not easily generate class property docs; pull requests welcome. - -.. automodule:: zero_ex.order_utils.signature_utils - :members: - Indices and tables ================== 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 index 9afeacfdf..3fec775b0 100644 --- 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 @@ -84,7 +84,6 @@ def method_id(name: str, types: List[str]) -> str: def simple_encode(method: str, *args: Any) -> bytes: - # docstring considered all one line by pylint: disable=line-too-long r"""Encode a method ABI. >>> simple_encode("ERC20Token(address)", "0x1dc4c1cefef38a777b15aa20260a54e584b16c48") 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 index 08c1b0ea5..1dcfb39a9 100644 --- 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 @@ -2,6 +2,9 @@ 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. @@ -56,3 +59,31 @@ def assert_is_hex_string(value: Any, name: str) -> None: """ 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/order_utils/__init__.py b/python-packages/order_utils/src/zero_ex/order_utils/__init__.py index fb5bc2f5d..c736d3567 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,27 @@ just this purpose. To start it: ``docker run -d -p 8545:8545 0xorg/ganache-cli fence smart topic"``. """ +from enum import auto, Enum import json -from typing import Dict +from typing import Dict, Tuple from pkg_resources import resource_string from mypy_extensions import TypedDict -from eth_utils import is_address, keccak, to_checksum_address, to_bytes +from eth_utils import keccak, to_bytes, to_checksum_address from web3 import Web3 -from web3.utils import datatypes 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, +) -class Constants: # pylint: disable=too-few-public-methods + +class _Constants: """Static data used by order utilities.""" contract_name_to_abi = { @@ -71,6 +79,18 @@ class Constants: # pylint: disable=too-few-public-methods + 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.""" @@ -90,14 +110,18 @@ class Order(TypedDict): # pylint: disable=too-many-instance-attributes def make_empty_order() -> Order: - """Construct an empty order.""" + """Construct an empty order. + + Initializes all strings to "0x0000000000000000000000000000000000000000" + 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, + "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, @@ -108,9 +132,13 @@ def make_empty_order() -> Order: def generate_order_hash_hex(order: Order, exchange_address: str) -> str: - # docstring considered all one line by pylint: disable=line-too-long """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 `_. + :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( ... { ... 'maker_address': "0x0000000000000000000000000000000000000000", @@ -138,12 +166,12 @@ def generate_order_hash_hex(order: Order, exchange_address: str) -> str: return i.to_bytes(32, byteorder="big") eip712_domain_struct_hash = keccak( - Constants.eip712_domain_struct_header + _Constants.eip712_domain_struct_header + pad_20_bytes_to_32(to_bytes(hexstr=exchange_address)) ) eip712_order_struct_hash = keccak( - Constants.eip712_order_schema_hash + _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"])) @@ -159,7 +187,202 @@ def generate_order_hash_hex(order: Order, exchange_address: str) -> str: ) return keccak( - Constants.eip191_header + _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 index e6f9a07c1..fab7479d2 100644 --- 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 @@ -47,7 +47,6 @@ def encode_erc20_asset_data(token_address: str) -> str: def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData: - # docstring considered all one line by pylint: disable=line-too-long """Decode an ERC20 asset data hex string. :param asset_data: String produced by prior call to encode_erc20_asset_data() @@ -82,7 +81,6 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData: def encode_erc721_asset_data(token_address: str, token_id: int) -> str: - # docstring considered all one line by pylint: disable=line-too-long """Encode an ERC721 asset data hex string. :param token_address: the ERC721 token's contract address. @@ -105,7 +103,6 @@ def encode_erc721_asset_data(token_address: str, token_id: int) -> str: def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData: - # docstring considered all one line by pylint: disable=line-too-long """Decode an ERC721 asset data hex string. >>> decode_erc721_asset_data('0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001') @@ -121,7 +118,6 @@ def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData: ) asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH] - # prefer `black` formatting. pylint: disable=C0330 if asset_proxy_id != abi_utils.method_id( "ERC721Token", ["address", "uint256"] ): diff --git a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py deleted file mode 100644 index 2e75be6d5..000000000 --- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Signature utilities.""" - -from typing import Tuple - -from eth_utils import is_address, to_checksum_address -from web3 import Web3 -import web3.exceptions -from web3.utils import datatypes - -from zero_ex.order_utils import Constants -from zero_ex.dev_utils.type_assertions import assert_is_hex_string - - -# prefer `black` formatting. pylint: disable=C0330 -def is_valid_signature( - provider: Web3.HTTPProvider, data: str, signature: str, signer_address: str -) -> Tuple[bool, str]: - # docstring considered all one line by pylint: disable=line-too-long - """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: Boolean indicating whether the given signature is valid. - - >>> is_valid_signature( - ... Web3.HTTPProvider("http://127.0.0.1:8545"), - ... '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0', - ... '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403', - ... '0x5409ed021d9299bf6814279a6a1411a7e866a631', - ... ) - (True, '') - """ # noqa: E501 (line too long) - # TODO: make this provider check more flexible. pylint: disable=fixme - # https://app.asana.com/0/684263176955174/901300863045491/f - if not isinstance(provider, Web3.HTTPProvider): - raise TypeError("provider is not a Web3.HTTPProvider") - assert_is_hex_string(data, "data") - assert_is_hex_string(signature, "signature") - assert_is_hex_string(signer_address, "signer_address") - if not is_address(signer_address): - raise ValueError("signer_address is not a valid 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_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}") diff --git a/python-packages/order_utils/stubs/web3/__init__.pyi b/python-packages/order_utils/stubs/web3/__init__.pyi index fcecc7434..b2af95475 100644 --- a/python-packages/order_utils/stubs/web3/__init__.pyi +++ b/python-packages/order_utils/stubs/web3/__init__.pyi @@ -1,12 +1,14 @@ from typing import Dict, Optional, Union from web3.utils import datatypes +from web3.providers.base import BaseProvider class Web3: - class HTTPProvider: ... + class HTTPProvider(BaseProvider): + ... - def __init__(self, provider: HTTPProvider) -> None: ... + def __init__(self, provider: BaseProvider) -> None: ... @staticmethod def sha3( diff --git a/python-packages/order_utils/stubs/web3/providers/__init__.pyi b/python-packages/order_utils/stubs/web3/providers/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/python-packages/order_utils/stubs/web3/providers/base.pyi b/python-packages/order_utils/stubs/web3/providers/base.pyi new file mode 100644 index 000000000..82ca9e3da --- /dev/null +++ b/python-packages/order_utils/stubs/web3/providers/base.pyi @@ -0,0 +1,2 @@ +class BaseProvider: + ... diff --git a/python-packages/order_utils/test/test_doctest.py b/python-packages/order_utils/test/test_doctest.py index 2b0350ac0..f692b3b6c 100644 --- a/python-packages/order_utils/test/test_doctest.py +++ b/python-packages/order_utils/test/test_doctest.py @@ -8,7 +8,6 @@ import zero_ex def test_all_doctests(): """Gather zero_ex.* modules and doctest them.""" - # prefer `black` formatting. pylint: disable=bad-continuation for (importer, modname, _) in pkgutil.walk_packages( path=zero_ex.__path__, prefix="zero_ex." ): 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 e393f38d7..af78d208c 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 @@ -3,7 +3,7 @@ from zero_ex.order_utils import ( generate_order_hash_hex, make_empty_order, - Constants, + _Constants, ) @@ -13,6 +13,6 @@ def test_get_order_hash_hex__empty_order(): "faa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422" ) actual_hash_hex = generate_order_hash_hex( - make_empty_order(), Constants.null_address + make_empty_order(), _Constants.null_address ) assert actual_hash_hex == expected_hash_hex diff --git a/python-packages/order_utils/test/test_signature_utils.py b/python-packages/order_utils/test/test_signature_utils.py index b688e03a1..c5acc9d62 100644 --- a/python-packages/order_utils/test/test_signature_utils.py +++ b/python-packages/order_utils/test/test_signature_utils.py @@ -3,7 +3,7 @@ import pytest from web3 import Web3 -from zero_ex.order_utils.signature_utils import is_valid_signature +from zero_ex.order_utils import is_valid_signature def test_is_valid_signature__provider_wrong_type(): -- cgit v1.2.3 From b961cb195299ce6a091ae692ec815b52f6b89300 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 14 Nov 2018 12:56:31 -0500 Subject: fix(order_utils.py): validate order w/json schema (#1260) --- python-packages/order_utils/setup.py | 3 +- python-packages/order_utils/src/index.rst | 3 + .../src/zero_ex/json_schemas/__init__.py | 61 ++++++++++++ .../order_utils/src/zero_ex/json_schemas/schemas | 1 + .../src/zero_ex/order_utils/__init__.py | 108 +++++++++++---------- .../order_utils/stubs/jsonschema/__init__.pyi | 5 + .../order_utils/stubs/jsonschema/exceptions.pyi | 0 .../test/test_generate_order_hash_hex.py | 10 +- 8 files changed, 130 insertions(+), 61 deletions(-) create mode 100644 python-packages/order_utils/src/zero_ex/json_schemas/__init__.py create mode 120000 python-packages/order_utils/src/zero_ex/json_schemas/schemas create mode 100644 python-packages/order_utils/stubs/jsonschema/__init__.pyi create mode 100644 python-packages/order_utils/stubs/jsonschema/exceptions.pyi (limited to 'python-packages') 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 + `_. + + 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 `_. @@ -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 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 -- cgit v1.2.3 From daf5719f082f62e46073b1badd141da445b33b09 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 14 Nov 2018 17:00:41 -0500 Subject: fix(order_utils.py): lazy load contract artifacts (#1262) --- .../src/zero_ex/order_utils/__init__.py | 29 ++++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) (limited to 'python-packages') 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 a86128e0e..24c6bfd4e 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 @@ -34,13 +34,26 @@ from zero_ex.json_schemas import assert_valid class _Constants: """Static data used by order utilities.""" - contract_name_to_abi = { - "Exchange": json.loads( - resource_string( - "zero_ex.contract_artifacts", "artifacts/Exchange.json" - ) - )["compilerOutput"]["abi"] - } + _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", @@ -233,7 +246,7 @@ def is_valid_signature( # 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=_Constants.contract_name_to_abi("Exchange"), ) try: return ( -- cgit v1.2.3