From 7b4f63a39ca4b4e05123d2b6871c6e01f8a132a2 Mon Sep 17 00:00:00 2001
From: "F. Eugene Aumson" <feuGeneA@users.noreply.github.com>
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 v1.2.3