aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils/src/asset_data_utils.ts
blob: f314891e20fac374f574e8501ceb68ca0d379891 (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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import {
    AssetProxyId,
    ERC20AssetData,
    ERC721AssetData,
    MultiAssetData,
    MultiAssetDataWithRecursiveDecoding,
    SingleAssetData,
} from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import * as _ from 'lodash';

import { constants } from './constants';

const encodingRules: AbiEncoder.EncodingRules = { shouldOptimize: true };
const decodingRules: AbiEncoder.DecodingRules = { shouldConvertStructsToObjects: true };

export const assetDataUtils = {
    /**
     * Encodes an ERC20 token address into a hex encoded assetData string, usable in the makerAssetData or
     * takerAssetData fields in a 0x order.
     * @param tokenAddress  The ERC20 token address to encode
     * @return The hex encoded assetData string
     */
    encodeERC20AssetData(tokenAddress: string): string {
        const abiEncoder = new AbiEncoder.Method(constants.ERC20_METHOD_ABI);
        const args = [tokenAddress];
        const assetData = abiEncoder.encode(args, encodingRules);
        return assetData;
    },
    /**
     * Decodes an ERC20 assetData hex string into it's corresponding ERC20 tokenAddress & assetProxyId
     * @param assetData Hex encoded assetData string to decode
     * @return An object containing the decoded tokenAddress & assetProxyId
     */
    decodeERC20AssetData(assetData: string): ERC20AssetData {
        assetDataUtils.assertIsERC20AssetData(assetData);
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        const abiEncoder = new AbiEncoder.Method(constants.ERC20_METHOD_ABI);
        const decodedAssetData = abiEncoder.decode(assetData, decodingRules);
        return {
            assetProxyId,
            // TODO(abandeali1): fix return types for `AbiEncoder.Method.decode` so that we can remove type assertion
            tokenAddress: (decodedAssetData as any).tokenContract,
        };
    },
    /**
     * Encodes an ERC721 token address into a hex encoded assetData string, usable in the makerAssetData or
     * takerAssetData fields in a 0x order.
     * @param tokenAddress  The ERC721 token address to encode
     * @param tokenId  The ERC721 tokenId to encode
     * @return The hex encoded assetData string
     */
    encodeERC721AssetData(tokenAddress: string, tokenId: BigNumber): string {
        const abiEncoder = new AbiEncoder.Method(constants.ERC721_METHOD_ABI);
        const args = [tokenAddress, tokenId];
        const assetData = abiEncoder.encode(args, encodingRules);
        return assetData;
    },
    /**
     * Decodes an ERC721 assetData hex string into it's corresponding ERC721 tokenAddress, tokenId & assetProxyId
     * @param assetData Hex encoded assetData string to decode
     * @return An object containing the decoded tokenAddress, tokenId & assetProxyId
     */
    decodeERC721AssetData(assetData: string): ERC721AssetData {
        assetDataUtils.assertIsERC721AssetData(assetData);
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        const abiEncoder = new AbiEncoder.Method(constants.ERC721_METHOD_ABI);
        const decodedAssetData = abiEncoder.decode(assetData, decodingRules);
        return {
            assetProxyId,
            // TODO(abandeali1): fix return types for `AbiEncoder.Method.decode` so that we can remove type assertion
            tokenAddress: (decodedAssetData as any).tokenContract,
            tokenId: (decodedAssetData as any).tokenId,
        };
    },
    /**
     * Encodes assetData for multiple AssetProxies into a single hex encoded assetData string, usable in the makerAssetData or
     * takerAssetData fields in a 0x order.
     * @param amounts Amounts of each asset that correspond to a single unit within an order.
     * @param nestedAssetData assetData strings that correspond to a valid assetProxyId.
     * @return The hex encoded assetData string
     */
    encodeMultiAssetData(amounts: BigNumber[], nestedAssetData: string[]): string {
        if (amounts.length !== nestedAssetData.length) {
            throw new Error(
                `Invalid MultiAsset arguments. Expected length of 'amounts' (${
                    amounts.length
                }) to equal length of 'nestedAssetData' (${nestedAssetData.length})`,
            );
        }
        _.forEach(nestedAssetData, assetDataElement => assetDataUtils.validateAssetDataOrThrow(assetDataElement));
        const abiEncoder = new AbiEncoder.Method(constants.MULTI_ASSET_METHOD_ABI);
        const args = [amounts, nestedAssetData];
        const assetData = abiEncoder.encode(args, encodingRules);
        return assetData;
    },
    /**
     * Decodes a MultiAsset assetData hex string into it's corresponding amounts and nestedAssetData
     * @param assetData Hex encoded assetData string to decode
     * @return An object containing the decoded amounts and nestedAssetData
     */
    decodeMultiAssetData(assetData: string): MultiAssetData {
        assetDataUtils.assertIsMultiAssetData(assetData);
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        const abiEncoder = new AbiEncoder.Method(constants.MULTI_ASSET_METHOD_ABI);
        const decodedAssetData = abiEncoder.decode(assetData, decodingRules);
        // TODO(abandeali1): fix return types for `AbiEncoder.Method.decode` so that we can remove type assertion
        const amounts = (decodedAssetData as any).amounts;
        const nestedAssetData = (decodedAssetData as any).nestedAssetData;
        if (amounts.length !== nestedAssetData.length) {
            throw new Error(
                `Invalid MultiAsset assetData. Expected length of 'amounts' (${
                    amounts.length
                }) to equal length of 'nestedAssetData' (${nestedAssetData.length})`,
            );
        }
        return {
            assetProxyId,
            amounts,
            nestedAssetData,
        };
    },
    /**
     * Decodes a MultiAsset assetData hex string into it's corresponding amounts and decoded nestedAssetData elements (all nested elements are flattened)
     * @param assetData Hex encoded assetData string to decode
     * @return An object containing the decoded amounts and nestedAssetData
     */
    decodeMultiAssetDataRecursively(assetData: string): MultiAssetDataWithRecursiveDecoding {
        const decodedAssetData = assetDataUtils.decodeMultiAssetData(assetData);
        const amounts: any[] = [];
        const decodedNestedAssetData = _.map(
            decodedAssetData.nestedAssetData as string[],
            (nestedAssetDataElement, index) => {
                const decodedNestedAssetDataElement = assetDataUtils.decodeAssetDataOrThrow(nestedAssetDataElement);
                if (decodedNestedAssetDataElement.assetProxyId === AssetProxyId.MultiAsset) {
                    const recursivelyDecodedAssetData = assetDataUtils.decodeMultiAssetDataRecursively(
                        nestedAssetDataElement,
                    );
                    amounts.push(
                        _.map(recursivelyDecodedAssetData.amounts, amountElement =>
                            amountElement.times(decodedAssetData.amounts[index]),
                        ),
                    );
                    return recursivelyDecodedAssetData.nestedAssetData;
                } else {
                    amounts.push(decodedAssetData.amounts[index]);
                    return decodedNestedAssetDataElement as SingleAssetData;
                }
            },
        );
        const flattenedAmounts = _.flattenDeep(amounts);
        const flattenedDecodedNestedAssetData = _.flattenDeep(decodedNestedAssetData);
        return {
            assetProxyId: decodedAssetData.assetProxyId,
            amounts: flattenedAmounts,
            // tslint:disable-next-line:no-unnecessary-type-assertion
            nestedAssetData: flattenedDecodedNestedAssetData as SingleAssetData[],
        };
    },
    /**
     * Decode and return the assetProxyId from the assetData
     * @param assetData Hex encoded assetData string to decode
     * @return The assetProxyId
     */
    decodeAssetProxyId(assetData: string): AssetProxyId {
        if (assetData.length < constants.SELECTOR_CHAR_LENGTH_WITH_PREFIX) {
            throw new Error(
                `Could not decode assetData. Expected length of encoded data to be at least 10. Got ${
                    assetData.length
                }`,
            );
        }
        const assetProxyId = assetData.slice(0, constants.SELECTOR_CHAR_LENGTH_WITH_PREFIX);
        if (
            assetProxyId !== AssetProxyId.ERC20 &&
            assetProxyId !== AssetProxyId.ERC721 &&
            assetProxyId !== AssetProxyId.MultiAsset
        ) {
            throw new Error(`Invalid assetProxyId: ${assetProxyId}`);
        }
        return assetProxyId;
    },
    /**
     * Checks if the decoded asset data is valid ERC20 data
     * @param decodedAssetData The decoded asset data to check
     */
    isERC20AssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is ERC20AssetData {
        return decodedAssetData.assetProxyId === AssetProxyId.ERC20;
    },
    /**
     * Checks if the decoded asset data is valid ERC721 data
     * @param decodedAssetData The decoded asset data to check
     */
    isERC721AssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is ERC721AssetData {
        return decodedAssetData.assetProxyId === AssetProxyId.ERC721;
    },
    /**
     * Checks if the decoded asset data is valid MultiAsset data
     * @param decodedAssetData The decoded asset data to check
     */
    isMultiAssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is MultiAssetData {
        return decodedAssetData.assetProxyId === AssetProxyId.MultiAsset;
    },
    /**
     * Throws if the length or assetProxyId are invalid for the ERC20Proxy.
     * @param assetData Hex encoded assetData string
     */
    assertIsERC20AssetData(assetData: string): void {
        if (assetData.length < constants.ERC20_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX) {
            throw new Error(
                `Could not decode ERC20 Proxy Data. Expected length of encoded data to be at least ${
                    constants.ERC20_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX
                }. Got ${assetData.length}`,
            );
        }
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        if (assetProxyId !== AssetProxyId.ERC20) {
            throw new Error(
                `Could not decode ERC20 assetData. Expected assetProxyId to be ERC20 (${
                    AssetProxyId.ERC20
                }), but got ${assetProxyId}`,
            );
        }
    },
    /**
     * Throws if the length or assetProxyId are invalid for the ERC721Proxy.
     * @param assetData Hex encoded assetData string
     */
    assertIsERC721AssetData(assetData: string): void {
        if (assetData.length < constants.ERC721_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX) {
            throw new Error(
                `Could not decode ERC721 assetData. Expected length of encoded data to be at least ${
                    constants.ERC721_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX
                }. Got ${assetData.length}`,
            );
        }
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        if (assetProxyId !== AssetProxyId.ERC721) {
            throw new Error(
                `Could not decode ERC721 assetData. Expected assetProxyId to be ERC721 (${
                    AssetProxyId.ERC721
                }), but got ${assetProxyId}`,
            );
        }
    },
    /**
     * Throws if the length or assetProxyId are invalid for the MultiAssetProxy.
     * @param assetData Hex encoded assetData string
     */
    assertIsMultiAssetData(assetData: string): void {
        if (assetData.length < constants.MULTI_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX) {
            throw new Error(
                `Could not decode MultiAsset assetData. Expected length of encoded data to be at least ${
                    constants.MULTI_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX
                }. Got ${assetData.length}`,
            );
        }
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        if (assetProxyId !== AssetProxyId.MultiAsset) {
            throw new Error(
                `Could not decode MultiAsset assetData. Expected assetProxyId to be MultiAsset (${
                    AssetProxyId.MultiAsset
                }), but got ${assetProxyId}`,
            );
        }
    },
    /**
     * Throws if the length or assetProxyId are invalid for the corresponding AssetProxy.
     * @param assetData Hex encoded assetData string
     */
    validateAssetDataOrThrow(assetData: string): void {
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        switch (assetProxyId) {
            case AssetProxyId.ERC20:
                assetDataUtils.assertIsERC20AssetData(assetData);
                break;
            case AssetProxyId.ERC721:
                assetDataUtils.assertIsERC721AssetData(assetData);
                break;
            case AssetProxyId.MultiAsset:
                assetDataUtils.assertIsMultiAssetData(assetData);
                break;
            default:
                throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`);
        }
    },
    /**
     * Decode any assetData into it's corresponding assetData object
     * @param assetData Hex encoded assetData string to decode
     * @return Either a ERC20 or ERC721 assetData object
     */
    decodeAssetDataOrThrow(assetData: string): SingleAssetData | MultiAssetData {
        const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
        switch (assetProxyId) {
            case AssetProxyId.ERC20:
                const erc20AssetData = assetDataUtils.decodeERC20AssetData(assetData);
                return erc20AssetData;
            case AssetProxyId.ERC721:
                const erc721AssetData = assetDataUtils.decodeERC721AssetData(assetData);
                return erc721AssetData;
            case AssetProxyId.MultiAsset:
                const multiAssetData = assetDataUtils.decodeMultiAssetData(assetData);
                return multiAssetData;
            default:
                throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`);
        }
    },
};