diff options
Diffstat (limited to 'python-packages/order_utils')
19 files changed, 529 insertions, 106 deletions
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 1b07b612c..679bfb4b2 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.""" @@ -160,7 +156,13 @@ setup( "publish": PublishCommand, "ganache": GanacheCommand, }, - install_requires=["eth-abi", "eth_utils", "mypy_extensions", "web3"], + install_requires=[ + "eth-abi", + "eth_utils", + "jsonschema", + "mypy_extensions", + "web3", + ], extras_require={ "dev": [ "bandit", @@ -182,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 b99addabd..551487ab1 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,9 +22,7 @@ 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 +.. automodule:: zero_ex.json_schemas :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 71b6128ca..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 @@ -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 @@ -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/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 80445cb6e..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 @@ -9,3 +9,397 @@ 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"``. """ + +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 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 12525ba88..000000000 --- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Signature utilities.""" - -from typing import Dict, Tuple -import json -from pkg_resources import resource_string - -from eth_utils import is_address, to_checksum_address -from web3 import Web3 -import web3.exceptions -from web3.utils import datatypes - -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 -) -> 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 = 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 - ) - 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/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/stubs/sha3/__init__.pyi b/python-packages/order_utils/stubs/sha3/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/sha3/__init__.pyi 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 --- /dev/null +++ b/python-packages/order_utils/stubs/web3/providers/__init__.pyi 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 new file mode 100644 index 000000000..6869a40ed --- /dev/null +++ b/python-packages/order_utils/test/test_generate_order_hash_hex.py @@ -0,0 +1,12 @@ +"""Test zero_ex.order_utils.get_order_hash_hex().""" + +from zero_ex.order_utils import generate_order_hash_hex, make_empty_order + + +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()) + 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(): |