feat(order_utils.py): ERC721 asset data codec (#1186)
This commit is contained in:
parent
0f63071696
commit
af91a56a55
@ -1,6 +1,7 @@
|
|||||||
import * as chai from 'chai';
|
import * as chai from 'chai';
|
||||||
|
|
||||||
import { ERC20AssetData } from '@0x/types';
|
import { ERC20AssetData, ERC721AssetData } from '@0x/types';
|
||||||
|
import { BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
import { assetDataUtils } from '../src/asset_data_utils';
|
import { assetDataUtils } from '../src/asset_data_utils';
|
||||||
|
|
||||||
@ -14,18 +15,36 @@ const KNOWN_ENCODINGS = [
|
|||||||
address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48',
|
address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48',
|
||||||
assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48',
|
assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48',
|
||||||
|
tokenId: new BigNumber(1),
|
||||||
|
assetData:
|
||||||
|
'0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ERC20_ASSET_PROXY_ID = '0xf47261b0';
|
const ERC20_ASSET_PROXY_ID = '0xf47261b0';
|
||||||
|
const ERC721_ASSET_PROXY_ID = '0x02571792';
|
||||||
|
|
||||||
describe('assetDataUtils', () => {
|
describe('assetDataUtils', () => {
|
||||||
it('should encode', () => {
|
it('should encode ERC20', () => {
|
||||||
const assetData = assetDataUtils.encodeERC20AssetData(KNOWN_ENCODINGS[0].address);
|
const assetData = assetDataUtils.encodeERC20AssetData(KNOWN_ENCODINGS[0].address);
|
||||||
expect(assetData).to.equal(KNOWN_ENCODINGS[0].assetData);
|
expect(assetData).to.equal(KNOWN_ENCODINGS[0].assetData);
|
||||||
});
|
});
|
||||||
it('should decode', () => {
|
it('should decode ERC20', () => {
|
||||||
const assetData: ERC20AssetData = assetDataUtils.decodeERC20AssetData(KNOWN_ENCODINGS[0].assetData);
|
const assetData: ERC20AssetData = assetDataUtils.decodeERC20AssetData(KNOWN_ENCODINGS[0].assetData);
|
||||||
expect(assetData.tokenAddress).to.equal(KNOWN_ENCODINGS[0].address);
|
expect(assetData.tokenAddress).to.equal(KNOWN_ENCODINGS[0].address);
|
||||||
expect(assetData.assetProxyId).to.equal(ERC20_ASSET_PROXY_ID);
|
expect(assetData.assetProxyId).to.equal(ERC20_ASSET_PROXY_ID);
|
||||||
});
|
});
|
||||||
|
it('should encode ERC721', () => {
|
||||||
|
const assetData = assetDataUtils.encodeERC721AssetData(KNOWN_ENCODINGS[1].address, KNOWN_ENCODINGS[1]
|
||||||
|
.tokenId as BigNumber);
|
||||||
|
expect(assetData).to.equal(KNOWN_ENCODINGS[1].assetData);
|
||||||
|
});
|
||||||
|
it('should decode ERC721', () => {
|
||||||
|
const assetData: ERC721AssetData = assetDataUtils.decodeERC721AssetData(KNOWN_ENCODINGS[1].assetData);
|
||||||
|
expect(assetData.tokenAddress).to.equal(KNOWN_ENCODINGS[1].address);
|
||||||
|
expect(assetData.assetProxyId).to.equal(ERC721_ASSET_PROXY_ID);
|
||||||
|
expect(assetData.tokenId).to.be.bignumber.equal(KNOWN_ENCODINGS[1].tokenId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ class TestCommandExtension(TestCommand):
|
|||||||
"""Invoke pytest."""
|
"""Invoke pytest."""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytest.main()
|
exit(pytest.main())
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
# pylint: disable=too-many-ancestors
|
||||||
|
@ -15,7 +15,9 @@ Python zero_ex.order_utils
|
|||||||
|
|
||||||
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
|
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
|
||||||
|
|
||||||
See source for properties. Sphinx does not easily generate class property docs; pull requests welcome.
|
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData
|
||||||
|
|
||||||
|
See source for class properties. Sphinx does not easily generate class property docs; pull requests welcome.
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
@ -31,3 +31,18 @@ def assert_is_list(value: Any, name: str) -> None:
|
|||||||
f"expected variable '{name}', with value {str(value)}, to have"
|
f"expected variable '{name}', with value {str(value)}, to have"
|
||||||
+ f" type 'list', not '{type(value).__name__}'"
|
+ f" type 'list', not '{type(value).__name__}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_is_int(value: Any, name: str) -> None:
|
||||||
|
"""If :param value: isn't of type int, raise a TypeError.
|
||||||
|
|
||||||
|
>>> try: assert_is_int('asdf', 'var')
|
||||||
|
... except TypeError as type_error: print(str(type_error))
|
||||||
|
...
|
||||||
|
expected variable 'var', with value asdf, to have type 'int', not 'str'
|
||||||
|
"""
|
||||||
|
if not isinstance(value, int):
|
||||||
|
raise TypeError(
|
||||||
|
f"expected variable '{name}', with value {str(value)}, to have"
|
||||||
|
+ f" type 'int', not '{type(value).__name__}'"
|
||||||
|
)
|
||||||
|
@ -5,10 +5,11 @@ from mypy_extensions import TypedDict
|
|||||||
import eth_abi
|
import eth_abi
|
||||||
|
|
||||||
from zero_ex.dev_utils import abi_utils
|
from zero_ex.dev_utils import abi_utils
|
||||||
from zero_ex.dev_utils.type_assertions import assert_is_string
|
from zero_ex.dev_utils.type_assertions import assert_is_string, assert_is_int
|
||||||
|
|
||||||
|
|
||||||
ERC20_ASSET_DATA_BYTE_LENGTH = 36
|
ERC20_ASSET_DATA_BYTE_LENGTH = 36
|
||||||
|
ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53
|
||||||
SELECTOR_LENGTH = 10
|
SELECTOR_LENGTH = 10
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +20,14 @@ class ERC20AssetData(TypedDict):
|
|||||||
token_address: 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:
|
def encode_erc20_asset_data(token_address: str) -> str:
|
||||||
"""Encode an ERC20 token address into an asset data string.
|
"""Encode an ERC20 token address into an asset data string.
|
||||||
|
|
||||||
@ -39,7 +48,7 @@ def encode_erc20_asset_data(token_address: str) -> str:
|
|||||||
|
|
||||||
def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
|
def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
|
||||||
# docstring considered all one line by pylint: disable=line-too-long
|
# docstring considered all one line by pylint: disable=line-too-long
|
||||||
"""Decode an ERC20 assetData hex string.
|
"""Decode an ERC20 asset data hex string.
|
||||||
|
|
||||||
:param asset_data: String produced by prior call to encode_erc20_asset_data()
|
:param asset_data: String produced by prior call to encode_erc20_asset_data()
|
||||||
|
|
||||||
@ -55,7 +64,7 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
|
|||||||
+ f" Got {str(len(asset_data))}."
|
+ f" Got {str(len(asset_data))}."
|
||||||
)
|
)
|
||||||
|
|
||||||
asset_proxy_id: str = asset_data[0:10]
|
asset_proxy_id: str = asset_data[0:SELECTOR_LENGTH]
|
||||||
if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]):
|
if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be"
|
"Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be"
|
||||||
@ -70,3 +79,65 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
|
|||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
|
return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
|
||||||
|
|
||||||
|
|
||||||
|
def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
|
||||||
|
# docstring considered all one line by pylint: disable=line-too-long
|
||||||
|
"""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:
|
||||||
|
# docstring considered all one line by pylint: disable=line-too-long
|
||||||
|
"""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]
|
||||||
|
# prefer `black` formatting. pylint: disable=C0330
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from zero_ex.order_utils.asset_data_utils import (
|
from zero_ex.order_utils.asset_data_utils import (
|
||||||
encode_erc20_asset_data,
|
|
||||||
decode_erc20_asset_data,
|
decode_erc20_asset_data,
|
||||||
|
decode_erc721_asset_data,
|
||||||
|
encode_erc20_asset_data,
|
||||||
|
encode_erc721_asset_data,
|
||||||
ERC20_ASSET_DATA_BYTE_LENGTH,
|
ERC20_ASSET_DATA_BYTE_LENGTH,
|
||||||
|
ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -33,3 +36,37 @@ def test_decode_erc20_asset_data_invalid_proxy_id():
|
|||||||
decode_erc20_asset_data(
|
decode_erc20_asset_data(
|
||||||
"0xffffffff" + (" " * ERC20_ASSET_DATA_BYTE_LENGTH)
|
"0xffffffff" + (" " * ERC20_ASSET_DATA_BYTE_LENGTH)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_encode_erc721_asset_data_type_error_on_token_address():
|
||||||
|
"""Test that passing a non-string for token_address raises a TypeError."""
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
encode_erc721_asset_data(123, 123)
|
||||||
|
|
||||||
|
|
||||||
|
def test_encode_erc721_asset_data_type_error_on_token_id():
|
||||||
|
"""Test that passing a non-int for token_id raises a TypeError."""
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
encode_erc721_asset_data("asdf", "asdf")
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_erc721_asset_data_type_error():
|
||||||
|
"""Test that passing a non-string for asset_data raises a TypeError."""
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
decode_erc721_asset_data(123)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_erc721_asset_data_with_asset_data_too_short():
|
||||||
|
"""Test that passing in too short of a string raises a ValueError."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
decode_erc721_asset_data(
|
||||||
|
" " * (ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH - 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_erc721_asset_data_invalid_proxy_id():
|
||||||
|
"""Test that passing in too short of a string raises a ValueError."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
decode_erc721_asset_data(
|
||||||
|
"0xffffffff" + " " * (ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH - 1)
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user