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) --- packages/utils/test/sign_typed_data_utils_test.ts | 23 +++ 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 +++ 7 files changed, 208 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 diff --git a/packages/utils/test/sign_typed_data_utils_test.ts b/packages/utils/test/sign_typed_data_utils_test.ts index dcba08b04..3d2cb2496 100644 --- a/packages/utils/test/sign_typed_data_utils_test.ts +++ b/packages/utils/test/sign_typed_data_utils_test.ts @@ -136,5 +136,28 @@ describe('signTypedDataUtils', () => { const hashHex = `0x${hash}`; expect(hashHex).to.be.eq(orderSignTypedDataHashHex); }); + it('creates a hash of an uninitialized order', () => { + const uninitializedOrder = { + ...orderSignTypedData, + message: { + makerAddress: '0x0000000000000000000000000000000000000000', + takerAddress: '0x0000000000000000000000000000000000000000', + makerAssetAmount: 0, + takerAssetAmount: 0, + expirationTimeSeconds: 0, + makerFee: 0, + takerFee: 0, + feeRecipientAddress: '0x0000000000000000000000000000000000000000', + senderAddress: '0x0000000000000000000000000000000000000000', + salt: 0, + makerAssetData: '0x0000000000000000000000000000000000000000', + takerAssetData: '0x0000000000000000000000000000000000000000', + exchangeAddress: '0x0000000000000000000000000000000000000000', + }, + }; + const hash = signTypedDataUtils.generateTypedDataHash(uninitializedOrder).toString('hex'); + const hashHex = `0x${hash}`; + expect(hashHex).to.be.eq('0xfaa49b35faeb9197e9c3ba7a52075e6dad19739549f153b77dfcf59408a4b422'); + }); }); }); 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