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
This commit is contained in:
F. Eugene Aumson
2018-10-23 12:08:16 -04:00
committed by GitHub
parent 1ba207f1fe
commit 1f0c7f8fbe
23 changed files with 410 additions and 37 deletions

View File

@@ -0,0 +1 @@
"""Dev utils to be shared across 0x projects and packages."""

View File

@@ -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:]
)

View File

@@ -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__}'"
)