aboutsummaryrefslogtreecommitdiffstats
path: root/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py
blob: fab7479d2ea0cbafeef576b117f2e52a8608a538 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""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, assert_is_int


ERC20_ASSET_DATA_BYTE_LENGTH = 36
ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53
SELECTOR_LENGTH = 10


class ERC20AssetData(TypedDict):
    """Object interface to ERC20 asset data."""

    asset_proxy_id: str
    token_address: str


class ERC721AssetData(TypedDict):
    """Object interface to ERC721 asset data."""

    asset_proxy_id: str
    token_address: str
    token_id: int


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:
    """Decode an ERC20 asset data 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:SELECTOR_LENGTH]
    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}


def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
    """Encode an ERC721 asset data hex string.

    :param token_address: the ERC721 token's contract address.
    :param token_id: the identifier of the asset's instance of the token.
    :rtype: hex encoded asset data string, usable in the makerAssetData or
        takerAssetData fields in a 0x order.

    >>> encode_erc721_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 1)
    '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001'
    """  # noqa: E501 (line too long)
    assert_is_string(token_address, "token_address")
    assert_is_int(token_id, "token_id")

    return (
        "0x"
        + abi_utils.simple_encode(
            "ERC721Token(address,uint256)", token_address, token_id
        ).hex()
    )


def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData:
    """Decode an ERC721 asset data hex string.

    >>> decode_erc721_asset_data('0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001')
    {'asset_proxy_id': '0x02571792', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 'token_id': 1}
    """  # noqa: E501 (line too long)
    assert_is_string(asset_data, "asset_data")

    if len(asset_data) < ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH:
        raise ValueError(
            "Could not decode ERC721 Asset Data. Expected length of encoded"
            + f"data to be at least {ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH}. "
            + f"Got {len(asset_data)}."
        )

    asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH]
    if asset_proxy_id != abi_utils.method_id(
        "ERC721Token", ["address", "uint256"]
    ):
        raise ValueError(
            "Could not decode ERC721 Asset Data. Expected Asset Proxy Id to be"
            + f" ERC721 ("
            + f"{abi_utils.method_id('ERC721Token', ['address', 'uint256'])}"
            + f"), but got {asset_proxy_id}"
        )

    (token_address, token_id) = eth_abi.decode_abi(
        ["address", "uint256"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
    )

    return {
        "asset_proxy_id": asset_proxy_id,
        "token_address": token_address,
        "token_id": token_id,
    }