feat(order_utils.py): ERC721 asset data codec (#1186)
This commit is contained in:
		@@ -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)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user