aboutsummaryrefslogtreecommitdiffstats
path: root/python-packages/order_utils/src
diff options
context:
space:
mode:
authorF. Eugene Aumson <feuGeneA@users.noreply.github.com>2018-10-24 00:08:16 +0800
committerGitHub <noreply@github.com>2018-10-24 00:08:16 +0800
commit1f0c7f8fbeba90ac1f65c57ff58782051c751b3d (patch)
treec5239e139729d6e4e63454c24679e8559f7c7560 /python-packages/order_utils/src
parent1ba207f1fef4338682b4cc7e45af8c073e63d263 (diff)
downloaddexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.tar
dexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.tar.gz
dexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.tar.bz2
dexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.tar.lz
dexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.tar.xz
dexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.tar.zst
dexon-sol-tools-1f0c7f8fbeba90ac1f65c57ff58782051c751b3d.zip
feat(order_utils.py): ERC20 asset data encoding and decoding
In addition to the ERC20 codec, also: Stopped ignoring type errors on 3rd party imports, by including interface stubs for them; Removed the unimplemented signature-utils module, which was just a stand-in when the python project support was first put in place. https://github.com/0xProject/0x-monorepo/pull/1144
Diffstat (limited to 'python-packages/order_utils/src')
-rw-r--r--python-packages/order_utils/src/conf.py5
-rw-r--r--python-packages/order_utils/src/index.rst5
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/__init__.py1
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py102
-rw-r--r--python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py33
-rw-r--r--python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py72
-rw-r--r--python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py13
7 files changed, 216 insertions, 15 deletions
diff --git a/python-packages/order_utils/src/conf.py b/python-packages/order_utils/src/conf.py
index f3f15967c..e74a29d00 100644
--- a/python-packages/order_utils/src/conf.py
+++ b/python-packages/order_utils/src/conf.py
@@ -2,6 +2,9 @@
# Reference: http://www.sphinx-doc.org/en/master/config
+from typing import List
+
+
# pylint: disable=invalid-name
# because these variables are not named in upper case, as globals should be.
@@ -29,7 +32,7 @@ master_doc = "index" # The master toctree document.
language = None
-exclude_patterns = [] # type: ignore
+exclude_patterns: List[str] = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst
index cbc4c8409..e09abcca2 100644
--- a/python-packages/order_utils/src/index.rst
+++ b/python-packages/order_utils/src/index.rst
@@ -10,9 +10,12 @@ order_utils.py
.. automodule:: zero_ex.order_utils
:members:
-.. automodule:: zero_ex.order_utils.signature_utils
+.. automodule:: zero_ex.order_utils.asset_data_utils
:members:
+.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
+
+See source for properties. Sphinx does not easily generate class property docs; pull requests welcome.
Indices and tables
==================
diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py
new file mode 100644
index 000000000..b6a224d2c
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py
@@ -0,0 +1 @@
+"""Dev utils to be shared across 0x projects and packages."""
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
new file mode 100644
index 000000000..71b6128ca
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py
@@ -0,0 +1,102 @@
+"""Ethereum ABI utilities.
+
+Builds on the eth-abi package, adding some convenience methods like those found
+in npmjs.com/package/ethereumjs-abi. Ideally, all of this code should be
+pushed upstream into eth-abi.
+"""
+
+import re
+from typing import Any, List
+
+from mypy_extensions import TypedDict
+
+from eth_abi import encode_abi
+from web3 import Web3
+
+from .type_assertions import assert_is_string, assert_is_list
+
+
+class MethodSignature(TypedDict, total=False):
+ """Object interface to an ABI method signature."""
+
+ method: str
+ args: List[str]
+
+
+def parse_signature(signature: str) -> MethodSignature:
+ """Parse a method signature into its constituent parts.
+
+ >>> parse_signature("ERC20Token(address)")
+ {'method': 'ERC20Token', 'args': ['address']}
+ """
+ assert_is_string(signature, "signature")
+
+ matches = re.match(r"^(\w+)\((.+)\)$", signature)
+ if matches is None:
+ raise ValueError(f"Invalid method signature {signature}")
+ return {"method": matches[1], "args": matches[2].split(",")}
+
+
+def elementary_name(name: str) -> str:
+ """Convert from short to canonical names; barely implemented.
+
+ Modeled after ethereumjs-abi's ABI.elementaryName(), but only implemented
+ to support our particular use case and a few other simple ones.
+
+ >>> elementary_name("address")
+ 'address'
+ >>> elementary_name("uint")
+ 'uint256'
+ """
+ assert_is_string(name, "name")
+
+ return {
+ "int": "int256",
+ "uint": "uint256",
+ "fixed": "fixed128x128",
+ "ufixed": "ufixed128x128",
+ }.get(name, name)
+
+
+def event_id(name: str, types: List[str]) -> str:
+ """Return the Keccak-256 hash of the given method.
+
+ >>> event_id("ERC20Token", ["address"])
+ '0xf47261b06eedbfce68afd46d0f3c27c60b03faad319eaf33103611cf8f6456ad'
+ """
+ assert_is_string(name, "name")
+ assert_is_list(types, "types")
+
+ signature = f"{name}({','.join(list(map(elementary_name, types)))})"
+ return Web3.sha3(text=signature).hex()
+
+
+def method_id(name: str, types: List[str]) -> str:
+ """Return the 4-byte method identifier.
+
+ >>> method_id("ERC20Token", ["address"])
+ '0xf47261b0'
+ """
+ assert_is_string(name, "name")
+ assert_is_list(types, "types")
+
+ return event_id(name, types)[0:10]
+
+
+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")
+ b'\xf4ra\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\xc4\xc1\xce\xfe\xf3\x8aw{\x15\xaa &\nT\xe5\x84\xb1lH'
+ """ # noqa: E501 (line too long)
+ assert_is_string(method, "method")
+
+ signature: MethodSignature = parse_signature(method)
+
+ return bytes.fromhex(
+ (
+ method_id(signature["method"], signature["args"])
+ + encode_abi(signature["args"], args).hex()
+ )[2:]
+ )
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
new file mode 100644
index 000000000..745d014e6
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py
@@ -0,0 +1,33 @@
+"""Assertions for runtime type checking of function arguments."""
+
+from typing import Any
+
+
+def assert_is_string(value: Any, name: str) -> None:
+ """If :param value: isn't of type str, raise a TypeError.
+
+ >>> try: assert_is_string(123, 'var')
+ ... except TypeError as type_error: print(str(type_error))
+ ...
+ expected variable 'var', with value 123, to have type 'str', not 'int'
+ """
+ if not isinstance(value, str):
+ raise TypeError(
+ f"expected variable '{name}', with value {str(value)}, to have"
+ + f" type 'str', not '{type(value).__name__}'"
+ )
+
+
+def assert_is_list(value: Any, name: str) -> None:
+ """If :param value: isn't of type list, raise a TypeError.
+
+ >>> try: assert_is_list(123, 'var')
+ ... except TypeError as type_error: print(str(type_error))
+ ...
+ expected variable 'var', with value 123, to have type 'list', not 'int'
+ """
+ if not isinstance(value, list):
+ raise TypeError(
+ f"expected variable '{name}', with value {str(value)}, to have"
+ + f" type 'list', not '{type(value).__name__}'"
+ )
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
new file mode 100644
index 000000000..451de39af
--- /dev/null
+++ b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
@@ -0,0 +1,72 @@
+"""Asset data encoding and decoding utilities."""
+
+from mypy_extensions import TypedDict
+
+import eth_abi
+
+from zero_ex.dev_utils import abi_utils
+from zero_ex.dev_utils.type_assertions import assert_is_string
+
+
+ERC20_ASSET_DATA_BYTE_LENGTH = 36
+SELECTOR_LENGTH = 10
+
+
+class ERC20AssetData(TypedDict):
+ """Object interface to ERC20 asset data."""
+
+ asset_proxy_id: str
+ token_address: str
+
+
+def encode_erc20_asset_data(token_address: str) -> str:
+ """Encode an ERC20 token address into an asset data string.
+
+ :param token_address: the ERC20 token's contract address.
+ :rtype: hex encoded asset data string, usable in the makerAssetData or
+ takerAssetData fields in a 0x order.
+
+ >>> encode_erc20_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48')
+ '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48'
+ """
+ assert_is_string(token_address, "token_address")
+
+ return (
+ "0x"
+ + abi_utils.simple_encode("ERC20Token(address)", token_address).hex()
+ )
+
+
+def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
+ # docstring considered all one line by pylint: disable=line-too-long
+ """Decode an ERC20 assetData hex string.
+
+ :param asset_data: String produced by prior call to encode_erc20_asset_data()
+
+ >>> decode_erc20_asset_data("0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48")
+ {'asset_proxy_id': '0xf47261b0', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'}
+ """ # noqa: E501 (line too long)
+ assert_is_string(asset_data, "asset_data")
+
+ if len(asset_data) < ERC20_ASSET_DATA_BYTE_LENGTH:
+ raise ValueError(
+ "Could not decode ERC20 Proxy Data. Expected length of encoded"
+ + f" data to be at least {str(ERC20_ASSET_DATA_BYTE_LENGTH)}."
+ + f" Got {str(len(asset_data))}."
+ )
+
+ asset_proxy_id: str = asset_data[0:10]
+ if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]):
+ raise ValueError(
+ "Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be"
+ + f" ERC20 ({abi_utils.method_id('ERC20Token', ['address'])})"
+ + f" but got {asset_proxy_id}."
+ )
+
+ # workaround for https://github.com/PyCQA/pylint/issues/1498
+ # pylint: disable=unsubscriptable-object
+ token_address = eth_abi.decode_abi(
+ ["address"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
+ )[0]
+
+ return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
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 7f4697106..000000000
--- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""Signature utilities."""
-
-
-def ec_sign_order_hash():
- """Signs an orderHash.
-
- Returns its elliptic curve signature and signature type. This method
- currently supports TestRPC, Geth, and Parity above and below v1.6.6.
-
- >>> ec_sign_order_hash()
- 'stub return value'
- """
- return "stub return value"