diff options
Diffstat (limited to 'packages/order-utils')
-rw-r--r-- | packages/order-utils/CHANGELOG.json | 56 | ||||
-rw-r--r-- | packages/order-utils/CHANGELOG.md | 20 | ||||
-rw-r--r-- | packages/order-utils/package.json | 26 | ||||
-rw-r--r-- | packages/order-utils/src/asset_data_utils.ts | 293 | ||||
-rw-r--r-- | packages/order-utils/src/constants.ts | 66 | ||||
-rw-r--r-- | packages/order-utils/src/exchange_transfer_simulator.ts | 64 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 5 | ||||
-rw-r--r-- | packages/order-utils/src/order_state_utils.ts | 56 | ||||
-rw-r--r-- | packages/order-utils/src/signature_utils.ts | 2 | ||||
-rw-r--r-- | packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts | 2 | ||||
-rw-r--r-- | packages/order-utils/test/asset_data_utils_test.ts | 116 | ||||
-rw-r--r-- | packages/order-utils/test/order_hash_test.ts | 2 | ||||
-rw-r--r-- | packages/order-utils/test/utils/test_order_factory.ts | 4 |
13 files changed, 581 insertions, 131 deletions
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 55e7defda..292f7a57b 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,5 +1,61 @@ [ { + "version": "3.1.0", + "changes": [ + { + "note": + "Use new ABI encoder, add encoding/decoding logic for MultiAsset assetData, and add information to return values in orderStateUtils", + "pr": 1363 + } + ] + }, + { + "version": "3.0.7", + "changes": [ + { + "note": "Dependencies updated" + } + ], + "timestamp": 1544739608 + }, + { + "version": "3.0.6", + "changes": [ + { + "note": "Fix bug in wallet signature type verification", + "pr": 1414 + } + ], + "timestamp": 1544570656 + }, + { + "timestamp": 1544482891, + "version": "3.0.5", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { + "timestamp": 1543401373, + "version": "3.0.4", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { + "timestamp": 1542821676, + "version": "3.0.3", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { "timestamp": 1542208198, "version": "3.0.2", "changes": [ diff --git a/packages/order-utils/CHANGELOG.md b/packages/order-utils/CHANGELOG.md index edf1569a7..bc8b48767 100644 --- a/packages/order-utils/CHANGELOG.md +++ b/packages/order-utils/CHANGELOG.md @@ -5,6 +5,26 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v3.0.7 - _December 13, 2018_ + + * Dependencies updated + +## v3.0.6 - _December 11, 2018_ + + * Fix bug in wallet signature type verification (#1414) + +## v3.0.5 - _December 10, 2018_ + + * Dependencies updated + +## v3.0.4 - _November 28, 2018_ + + * Dependencies updated + +## v3.0.3 - _November 21, 2018_ + + * Dependencies updated + ## v3.0.2 - _November 14, 2018_ * Dependencies updated diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index fe5ea70b1..400c9b66f 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -1,6 +1,6 @@ { "name": "@0x/order-utils", - "version": "3.0.2", + "version": "3.0.7", "engines": { "node": ">=6.12" }, @@ -35,8 +35,8 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md", "devDependencies": { - "@0x/dev-utils": "^1.0.17", - "@0x/tslint-config": "^1.0.10", + "@0x/dev-utils": "^1.0.21", + "@0x/tslint-config": "^2.0.0", "@types/bn.js": "^4.11.0", "@types/lodash": "4.14.104", "chai": "^4.0.1", @@ -53,18 +53,18 @@ "typescript": "3.0.1" }, "dependencies": { - "@0x/abi-gen-wrappers": "^1.0.5", - "@0x/assert": "^1.0.17", - "@0x/base-contract": "^3.0.6", - "@0x/contract-artifacts": "^1.1.0", - "@0x/json-schemas": "^2.1.1", - "@0x/types": "^1.2.1", - "@0x/typescript-typings": "^3.0.4", - "@0x/utils": "^2.0.5", - "@0x/web3-wrapper": "^3.1.4", + "@0x/abi-gen-wrappers": "^2.0.2", + "@0x/assert": "^1.0.20", + "@0x/base-contract": "^3.0.10", + "@0x/contract-artifacts": "^1.1.2", + "@0x/json-schemas": "^2.1.4", + "@0x/types": "^1.4.1", + "@0x/typescript-typings": "^3.0.6", + "@0x/utils": "^2.0.8", + "@0x/web3-wrapper": "^3.2.1", "@types/node": "*", "bn.js": "^4.11.8", - "ethereum-types": "^1.1.2", + "ethereum-types": "^1.1.4", "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1", "ethers": "~4.0.4", diff --git a/packages/order-utils/src/asset_data_utils.ts b/packages/order-utils/src/asset_data_utils.ts index 9bbef3a23..f314891e2 100644 --- a/packages/order-utils/src/asset_data_utils.ts +++ b/packages/order-utils/src/asset_data_utils.ts @@ -1,10 +1,19 @@ -import { AssetData, AssetProxyId, ERC20AssetData, ERC721AssetData } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import ethAbi = require('ethereumjs-abi'); -import ethUtil = require('ethereumjs-util'); +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 @@ -13,7 +22,10 @@ export const assetDataUtils = { * @return The hex encoded assetData string */ encodeERC20AssetData(tokenAddress: string): string { - return ethUtil.bufferToHex(ethAbi.simpleEncode('ERC20Token(address)', tokenAddress)); + 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 @@ -21,26 +33,14 @@ export const assetDataUtils = { * @return An object containing the decoded tokenAddress & assetProxyId */ decodeERC20AssetData(assetData: string): ERC20AssetData { - const data = ethUtil.toBuffer(assetData); - if (data.byteLength < constants.ERC20_ASSET_DATA_BYTE_LENGTH) { - throw new Error( - `Could not decode ERC20 Proxy Data. Expected length of encoded data to be at least ${ - constants.ERC20_ASSET_DATA_BYTE_LENGTH - }. Got ${data.byteLength}`, - ); - } - const assetProxyId = ethUtil.bufferToHex(data.slice(0, constants.SELECTOR_LENGTH)); - if (assetProxyId !== AssetProxyId.ERC20) { - throw new Error( - `Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be ERC20 (${ - AssetProxyId.ERC20 - }), but got ${assetProxyId}`, - ); - } - const [tokenAddress] = ethAbi.rawDecode(['address'], data.slice(constants.SELECTOR_LENGTH)); + 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, - tokenAddress: ethUtil.addHexPrefix(tokenAddress), + // TODO(abandeali1): fix return types for `AbiEncoder.Method.decode` so that we can remove type assertion + tokenAddress: (decodedAssetData as any).tokenContract, }; }, /** @@ -51,14 +51,10 @@ export const assetDataUtils = { * @return The hex encoded assetData string */ encodeERC721AssetData(tokenAddress: string, tokenId: BigNumber): string { - // TODO: Pass `tokendId` as a BigNumber. - return ethUtil.bufferToHex( - ethAbi.simpleEncode( - 'ERC721Token(address,uint256)', - tokenAddress, - `0x${tokenId.toString(constants.BASE_16)}`, - ), - ); + 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 @@ -66,27 +62,99 @@ export const assetDataUtils = { * @return An object containing the decoded tokenAddress, tokenId & assetProxyId */ decodeERC721AssetData(assetData: string): ERC721AssetData { - const data = ethUtil.toBuffer(assetData); - if (data.byteLength < constants.ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH) { + 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( - `Could not decode ERC721 Asset Data. Expected length of encoded data to be at least ${ - constants.ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH - }. Got ${data.byteLength}`, + `Invalid MultiAsset arguments. Expected length of 'amounts' (${ + amounts.length + }) to equal length of 'nestedAssetData' (${nestedAssetData.length})`, ); } - const assetProxyId = ethUtil.bufferToHex(data.slice(0, constants.SELECTOR_LENGTH)); - if (assetProxyId !== AssetProxyId.ERC721) { + _.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( - `Could not decode ERC721 Asset Data. Expected Asset Proxy Id to be ERC721 (${ - AssetProxyId.ERC721 - }), but got ${assetProxyId}`, + `Invalid MultiAsset assetData. Expected length of 'amounts' (${ + amounts.length + }) to equal length of 'nestedAssetData' (${nestedAssetData.length})`, ); } - const [tokenAddress, tokenId] = ethAbi.rawDecode(['address', 'uint256'], data.slice(constants.SELECTOR_LENGTH)); return { assetProxyId, - tokenAddress: ethUtil.addHexPrefix(tokenAddress), - tokenId: new BigNumber(tokenId.toString()), + 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[], }; }, /** @@ -95,24 +163,133 @@ export const assetDataUtils = { * @return The assetProxyId */ decodeAssetProxyId(assetData: string): AssetProxyId { - const encodedAssetData = ethUtil.toBuffer(assetData); - if (encodedAssetData.byteLength < constants.SELECTOR_LENGTH) { + 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 4. Got ${ - encodedAssetData.byteLength + `Could not decode assetData. Expected length of encoded data to be at least 10. Got ${ + assetData.length }`, ); } - const encodedAssetProxyId = encodedAssetData.slice(0, constants.SELECTOR_LENGTH); - const assetProxyId = decodeAssetProxyId(encodedAssetProxyId); + 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): AssetData { + decodeAssetDataOrThrow(assetData: string): SingleAssetData | MultiAssetData { const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData); switch (assetProxyId) { case AssetProxyId.ERC20: @@ -121,19 +298,11 @@ export const assetDataUtils = { 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}`); } }, }; - -function decodeAssetProxyId(encodedAssetProxyId: Buffer): AssetProxyId { - const hexString = ethUtil.bufferToHex(encodedAssetProxyId); - if (hexString === AssetProxyId.ERC20) { - return AssetProxyId.ERC20; - } - if (hexString === AssetProxyId.ERC721) { - return AssetProxyId.ERC721; - } - throw new Error(`Invalid ProxyId: ${hexString}`); -} diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index 10029dcc3..a9a687719 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -1,16 +1,71 @@ import { BigNumber } from '@0x/utils'; +import { MethodAbi } from 'ethereum-types'; + +const ERC20_METHOD_ABI: MethodAbi = { + constant: false, + inputs: [ + { + name: 'tokenContract', + type: 'address', + }, + ], + name: 'ERC20Token', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +const ERC721_METHOD_ABI: MethodAbi = { + constant: false, + inputs: [ + { + name: 'tokenContract', + type: 'address', + }, + { + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'ERC721Token', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +const MULTI_ASSET_METHOD_ABI: MethodAbi = { + constant: false, + inputs: [ + { + name: 'amounts', + type: 'uint256[]', + }, + { + name: 'nestedAssetData', + type: 'bytes[]', + }, + ], + name: 'MultiAsset', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', NULL_BYTES: '0x', + NULL_ERC20_ASSET_DATA: '0xf47261b00000000000000000000000000000000000000000000000000000000000000000', // tslint:disable-next-line:custom-no-magic-numbers UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), TESTRPC_NETWORK_ID: 50, ADDRESS_LENGTH: 20, - ERC20_ASSET_DATA_BYTE_LENGTH: 36, - ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, - SELECTOR_LENGTH: 4, - BASE_16: 16, + ERC20_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX: 74, + ERC721_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX: 136, + MULTI_ASSET_DATA_MIN_CHAR_LENGTH_WITH_PREFIX: 266, + SELECTOR_CHAR_LENGTH_WITH_PREFIX: 10, INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite ZERO_AMOUNT: new BigNumber(0), EIP712_DOMAIN_NAME: '0x Protocol', @@ -48,4 +103,7 @@ export const constants = { { name: 'data', type: 'bytes' }, ], }, + ERC20_METHOD_ABI, + ERC721_METHOD_ABI, + MULTI_ASSET_METHOD_ABI, }; diff --git a/packages/order-utils/src/exchange_transfer_simulator.ts b/packages/order-utils/src/exchange_transfer_simulator.ts index 7a38b35df..06621fd9e 100644 --- a/packages/order-utils/src/exchange_transfer_simulator.ts +++ b/packages/order-utils/src/exchange_transfer_simulator.ts @@ -1,7 +1,8 @@ -import { ExchangeContractErrs } from '@0x/types'; +import { AssetProxyId, ExchangeContractErrs } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { AbstractBalanceAndProxyAllowanceLazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store'; +import { assetDataUtils } from './asset_data_utils'; import { constants } from './constants'; import { TradeSide, TransferType } from './types'; @@ -74,24 +75,51 @@ export class ExchangeTransferSimulator { tradeSide: TradeSide, transferType: TransferType, ): Promise<void> { - // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/ - // allowances for the taker. We do however, want to increase the balance of the maker since the maker - // might be relying on those funds to fill subsequent orders or pay the order's fees. - if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) { - await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); - return; + const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (assetProxyId) { + case AssetProxyId.ERC20: + case AssetProxyId.ERC721: + // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/ + // allowances for the taker. We do however, want to increase the balance of the maker since the maker + // might be relying on those funds to fill subsequent orders or pay the order's fees. + if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) { + await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); + return; + } + const balance = await this._store.getBalanceAsync(assetData, from); + const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, from); + if (proxyAllowance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError( + FailureReason.ProxyAllowance, + tradeSide, + transferType, + ); + } + if (balance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType); + } + await this._decreaseProxyAllowanceAsync(assetData, from, amountInBaseUnits); + await this._decreaseBalanceAsync(assetData, from, amountInBaseUnits); + await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); + break; + case AssetProxyId.MultiAsset: + const decodedAssetData = assetDataUtils.decodeMultiAssetData(assetData); + for (const [index, nestedAssetDataElement] of decodedAssetData.nestedAssetData.entries()) { + const amountsElement = decodedAssetData.amounts[index]; + const totalAmount = amountInBaseUnits.times(amountsElement); + await this.transferFromAsync( + nestedAssetDataElement, + from, + to, + totalAmount, + tradeSide, + transferType, + ); + } + break; + default: + break; } - const balance = await this._store.getBalanceAsync(assetData, from); - const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, from); - if (proxyAllowance.lessThan(amountInBaseUnits)) { - ExchangeTransferSimulator._throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType); - } - if (balance.lessThan(amountInBaseUnits)) { - ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType); - } - await this._decreaseProxyAllowanceAsync(assetData, from, amountInBaseUnits); - await this._decreaseBalanceAsync(assetData, from, amountInBaseUnits); - await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); } private async _decreaseProxyAllowanceAsync( assetData: string, diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index e70d43efb..2150a02e4 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -34,11 +34,14 @@ export { OrderRelevantState, OrderState, ECSignature, - AssetData, + SingleAssetData, ERC20AssetData, ERC721AssetData, + MultiAssetData, + MultiAssetDataWithRecursiveDecoding, AssetProxyId, SignatureType, + ObjectMap, OrderStateValid, OrderStateInvalid, ExchangeContractErrs, diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index fe0d6c773..389419587 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -1,5 +1,6 @@ import { ExchangeContractErrs, + ObjectMap, OrderRelevantState, OrderState, OrderStateInvalid, @@ -7,9 +8,11 @@ import { SignedOrder, } from '@0x/types'; import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; +import { assetDataUtils } from './asset_data_utils'; import { orderHashUtils } from './order_hash'; import { OrderValidationUtils } from './order_validation_utils'; import { RemainingFillableCalculator } from './remaining_fillable_calculator'; @@ -18,7 +21,9 @@ import { utils } from './utils'; interface SidedOrderRelevantState { isMakerSide: boolean; traderBalance: BigNumber; + traderIndividualBalances: ObjectMap<BigNumber>; traderProxyAllowance: BigNumber; + traderIndividualProxyAllowances: ObjectMap<BigNumber>; traderFeeBalance: BigNumber; traderFeeProxyAllowance: BigNumber; filledTakerAssetAmount: BigNumber; @@ -121,7 +126,9 @@ export class OrderStateUtils { const sidedOrderRelevantState = { isMakerSide: true, traderBalance: orderRelevantState.makerBalance, + traderIndividualBalances: orderRelevantState.makerIndividualBalances, traderProxyAllowance: orderRelevantState.makerProxyAllowance, + traderIndividualProxyAllowances: orderRelevantState.makerIndividualProxyAllowances, traderFeeBalance: orderRelevantState.makerFeeBalance, traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance, filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount, @@ -165,7 +172,9 @@ export class OrderStateUtils { const orderRelevantState = { makerBalance: sidedOrderRelevantState.traderBalance, + makerIndividualBalances: sidedOrderRelevantState.traderIndividualBalances, makerProxyAllowance: sidedOrderRelevantState.traderProxyAllowance, + makerIndividualProxyAllowances: sidedOrderRelevantState.traderIndividualProxyAllowances, makerFeeBalance: sidedOrderRelevantState.traderFeeBalance, makerFeeProxyAllowance: sidedOrderRelevantState.traderFeeProxyAllowance, filledTakerAssetAmount: sidedOrderRelevantState.filledTakerAssetAmount, @@ -236,10 +245,12 @@ export class OrderStateUtils { const isAssetZRX = assetData === zrxAssetData; const traderBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress); + const traderIndividualBalances = await this._getAssetBalancesAsync(assetData, traderAddress); const traderProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( assetData, traderAddress, ); + const traderIndividualProxyAllowances = await this._getAssetProxyAllowancesAsync(assetData, traderAddress); const traderFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( zrxAssetData, traderAddress, @@ -278,7 +289,9 @@ export class OrderStateUtils { const sidedOrderRelevantState = { isMakerSide, traderBalance, + traderIndividualBalances, traderProxyAllowance, + traderIndividualProxyAllowances, traderFeeBalance, traderFeeProxyAllowance, filledTakerAssetAmount, @@ -287,4 +300,47 @@ export class OrderStateUtils { }; return sidedOrderRelevantState; } + private async _getAssetBalancesAsync( + assetData: string, + traderAddress: string, + initialBalances: ObjectMap<BigNumber> = {}, + ): Promise<ObjectMap<BigNumber>> { + const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); + let balances: ObjectMap<BigNumber> = { ...initialBalances }; + if (assetDataUtils.isERC20AssetData(decodedAssetData) || assetDataUtils.isERC721AssetData(decodedAssetData)) { + const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress); + const tokenAddress = decodedAssetData.tokenAddress; + balances[tokenAddress] = _.isUndefined(initialBalances[tokenAddress]) + ? balance + : balances[tokenAddress].add(balance); + } else if (assetDataUtils.isMultiAssetData(decodedAssetData)) { + for (const assetDataElement of decodedAssetData.nestedAssetData) { + balances = await this._getAssetBalancesAsync(assetDataElement, traderAddress, balances); + } + } + return balances; + } + private async _getAssetProxyAllowancesAsync( + assetData: string, + traderAddress: string, + initialAllowances: ObjectMap<BigNumber> = {}, + ): Promise<ObjectMap<BigNumber>> { + const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); + let allowances: ObjectMap<BigNumber> = { ...initialAllowances }; + if (assetDataUtils.isERC20AssetData(decodedAssetData) || assetDataUtils.isERC721AssetData(decodedAssetData)) { + const allowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( + assetData, + traderAddress, + ); + const tokenAddress = decodedAssetData.tokenAddress; + allowances[tokenAddress] = _.isUndefined(initialAllowances[tokenAddress]) + ? allowance + : allowances[tokenAddress].add(allowance); + } else if (assetDataUtils.isMultiAssetData(decodedAssetData)) { + for (const assetDataElement of decodedAssetData.nestedAssetData) { + allowances = await this._getAssetBalancesAsync(assetDataElement, traderAddress, allowances); + } + } + return allowances; + } } diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index 96d90e21a..131144d48 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -115,7 +115,7 @@ export const signatureUtils = { assert.isHexString('signature', signature); assert.isETHAddressHex('signerAddress', signerAddress); // tslint:disable-next-line:custom-no-magic-numbers - const signatureWithoutType = signature.slice(-2); + const signatureWithoutType = signature.slice(0, -2); const walletContract = new IWalletContract(artifacts.IWallet.compilerOutput.abi, signerAddress, provider); const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType); return isValid; diff --git a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts index f42a76d0c..ae3e36238 100644 --- a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts +++ b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts @@ -119,7 +119,7 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx public deleteAllERC721ProxyAllowance(tokenAddress: string, userAddress: string): void { for (const assetData in this._proxyAllowance) { if (this._proxyAllowance.hasOwnProperty(assetData)) { - const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); + const decodedAssetData = assetDataUtils.decodeERC721AssetData(assetData); if ( decodedAssetData.assetProxyId === AssetProxyId.ERC721 && decodedAssetData.tokenAddress === tokenAddress && diff --git a/packages/order-utils/test/asset_data_utils_test.ts b/packages/order-utils/test/asset_data_utils_test.ts index f175b7a38..c498c5a00 100644 --- a/packages/order-utils/test/asset_data_utils_test.ts +++ b/packages/order-utils/test/asset_data_utils_test.ts @@ -1,6 +1,6 @@ import * as chai from 'chai'; -import { ERC20AssetData, ERC721AssetData } from '@0x/types'; +import { AssetProxyId, ERC721AssetData } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { assetDataUtils } from '../src/asset_data_utils'; @@ -10,41 +10,101 @@ import { chaiSetup } from './utils/chai_setup'; chaiSetup.configure(); const expect = chai.expect; -const KNOWN_ENCODINGS = [ - { - address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', - assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', - }, - { - address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', - tokenId: new BigNumber(1), - assetData: - '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001', - }, -]; - -const ERC20_ASSET_PROXY_ID = '0xf47261b0'; -const ERC721_ASSET_PROXY_ID = '0x02571792'; +const KNOWN_ERC20_ENCODING = { + address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', +}; +const KNOWN_ERC721_ENCODING = { + address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + tokenId: new BigNumber(1), + assetData: + '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001', +}; +const KNOWN_MULTI_ASSET_ENCODING = { + amounts: [new BigNumber(1), new BigNumber(1)], + nestedAssetData: [KNOWN_ERC20_ENCODING.assetData, KNOWN_ERC721_ENCODING.assetData], + assetData: + '0x94cfcdd7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024f47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000', +}; describe('assetDataUtils', () => { it('should encode ERC20', () => { - const assetData = assetDataUtils.encodeERC20AssetData(KNOWN_ENCODINGS[0].address); - expect(assetData).to.equal(KNOWN_ENCODINGS[0].assetData); + const assetData = assetDataUtils.encodeERC20AssetData(KNOWN_ERC20_ENCODING.address); + expect(assetData).to.equal(KNOWN_ERC20_ENCODING.assetData); }); it('should decode ERC20', () => { - const assetData: ERC20AssetData = assetDataUtils.decodeERC20AssetData(KNOWN_ENCODINGS[0].assetData); - expect(assetData.tokenAddress).to.equal(KNOWN_ENCODINGS[0].address); - expect(assetData.assetProxyId).to.equal(ERC20_ASSET_PROXY_ID); + const decodedAssetData = assetDataUtils.decodeERC20AssetData(KNOWN_ERC20_ENCODING.assetData); + expect(decodedAssetData.tokenAddress).to.equal(KNOWN_ERC20_ENCODING.address); + expect(decodedAssetData.assetProxyId).to.equal(AssetProxyId.ERC20); }); 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); + const assetData = assetDataUtils.encodeERC721AssetData( + KNOWN_ERC721_ENCODING.address, + KNOWN_ERC721_ENCODING.tokenId, + ); + expect(assetData).to.equal(KNOWN_ERC721_ENCODING.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); + const decodedAssetData = assetDataUtils.decodeERC721AssetData(KNOWN_ERC721_ENCODING.assetData); + expect(decodedAssetData.tokenAddress).to.equal(KNOWN_ERC721_ENCODING.address); + expect(decodedAssetData.assetProxyId).to.equal(AssetProxyId.ERC721); + expect(decodedAssetData.tokenId).to.be.bignumber.equal(KNOWN_ERC721_ENCODING.tokenId); + }); + it('should encode ERC20 and ERC721 multiAssetData', () => { + const assetData = assetDataUtils.encodeMultiAssetData( + KNOWN_MULTI_ASSET_ENCODING.amounts, + KNOWN_MULTI_ASSET_ENCODING.nestedAssetData, + ); + expect(assetData).to.equal(KNOWN_MULTI_ASSET_ENCODING.assetData); + }); + it('should decode ERC20 and ERC721 multiAssetData', () => { + const decodedAssetData = assetDataUtils.decodeMultiAssetData(KNOWN_MULTI_ASSET_ENCODING.assetData); + expect(decodedAssetData.assetProxyId).to.equal(AssetProxyId.MultiAsset); + expect(decodedAssetData.amounts).to.deep.equal(KNOWN_MULTI_ASSET_ENCODING.amounts); + expect(decodedAssetData.nestedAssetData).to.deep.equal(KNOWN_MULTI_ASSET_ENCODING.nestedAssetData); + }); + it('should recursively decode ERC20 and ERC721 multiAssetData', () => { + const decodedAssetData = assetDataUtils.decodeMultiAssetDataRecursively(KNOWN_MULTI_ASSET_ENCODING.assetData); + expect(decodedAssetData.assetProxyId).to.equal(AssetProxyId.MultiAsset); + expect(decodedAssetData.amounts).to.deep.equal(KNOWN_MULTI_ASSET_ENCODING.amounts); + const decodedErc20AssetData = decodedAssetData.nestedAssetData[0]; + // tslint:disable-next-line:no-unnecessary-type-assertion + const decodedErc721AssetData = decodedAssetData.nestedAssetData[1] as ERC721AssetData; + expect(decodedErc20AssetData.tokenAddress).to.equal(KNOWN_ERC20_ENCODING.address); + expect(decodedErc20AssetData.assetProxyId).to.equal(AssetProxyId.ERC20); + expect(decodedErc721AssetData.tokenAddress).to.equal(KNOWN_ERC721_ENCODING.address); + expect(decodedErc721AssetData.assetProxyId).to.equal(AssetProxyId.ERC721); + expect(decodedErc721AssetData.tokenId).to.be.bignumber.equal(KNOWN_ERC721_ENCODING.tokenId); + }); + it('should recursively decode nested assetData within multiAssetData', () => { + const amounts = [new BigNumber(1), new BigNumber(1), new BigNumber(2)]; + const nestedAssetData = [ + KNOWN_ERC20_ENCODING.assetData, + KNOWN_ERC721_ENCODING.assetData, + KNOWN_MULTI_ASSET_ENCODING.assetData, + ]; + const assetData = assetDataUtils.encodeMultiAssetData(amounts, nestedAssetData); + const decodedAssetData = assetDataUtils.decodeMultiAssetDataRecursively(assetData); + expect(decodedAssetData.assetProxyId).to.equal(AssetProxyId.MultiAsset); + const expectedAmounts = [new BigNumber(1), new BigNumber(1), new BigNumber(2), new BigNumber(2)]; + expect(decodedAssetData.amounts).to.deep.equal(expectedAmounts); + const expectedLength = 4; + expect(decodedAssetData.nestedAssetData.length).to.be.equal(expectedLength); + const decodedErc20AssetData1 = decodedAssetData.nestedAssetData[0]; + // tslint:disable-next-line:no-unnecessary-type-assertion + const decodedErc721AssetData1 = decodedAssetData.nestedAssetData[1] as ERC721AssetData; + const decodedErc20AssetData2 = decodedAssetData.nestedAssetData[2]; + // tslint:disable-next-line:no-unnecessary-type-assertion + const decodedErc721AssetData2 = decodedAssetData.nestedAssetData[3] as ERC721AssetData; + expect(decodedErc20AssetData1.tokenAddress).to.equal(KNOWN_ERC20_ENCODING.address); + expect(decodedErc20AssetData1.assetProxyId).to.equal(AssetProxyId.ERC20); + expect(decodedErc721AssetData1.tokenAddress).to.equal(KNOWN_ERC721_ENCODING.address); + expect(decodedErc721AssetData1.assetProxyId).to.equal(AssetProxyId.ERC721); + expect(decodedErc721AssetData1.tokenId).to.be.bignumber.equal(KNOWN_ERC721_ENCODING.tokenId); + expect(decodedErc20AssetData2.tokenAddress).to.equal(KNOWN_ERC20_ENCODING.address); + expect(decodedErc20AssetData2.assetProxyId).to.equal(AssetProxyId.ERC20); + expect(decodedErc721AssetData2.tokenAddress).to.equal(KNOWN_ERC721_ENCODING.address); + expect(decodedErc721AssetData2.assetProxyId).to.equal(AssetProxyId.ERC721); + expect(decodedErc721AssetData2.tokenId).to.be.bignumber.equal(KNOWN_ERC721_ENCODING.tokenId); }); }); diff --git a/packages/order-utils/test/order_hash_test.ts b/packages/order-utils/test/order_hash_test.ts index a85d4c81a..30fb15a37 100644 --- a/packages/order-utils/test/order_hash_test.ts +++ b/packages/order-utils/test/order_hash_test.ts @@ -70,7 +70,7 @@ describe('Order hashing', () => { }); it('returns true if order hash is correct', () => { const orderHashLength = 65; - const isValid = orderHashUtils.isValidOrderHash('0x' + Array(orderHashLength).join('0')); + const isValid = orderHashUtils.isValidOrderHash(`0x${Array(orderHashLength).join('0')}`); expect(isValid).to.be.true(); }); }); diff --git a/packages/order-utils/test/utils/test_order_factory.ts b/packages/order-utils/test/utils/test_order_factory.ts index 145332674..4efe0b38e 100644 --- a/packages/order-utils/test/utils/test_order_factory.ts +++ b/packages/order-utils/test/utils/test_order_factory.ts @@ -7,9 +7,9 @@ import { orderFactory } from '../../src/order_factory'; const BASE_TEST_ORDER: Order = orderFactory.createOrder( constants.NULL_ADDRESS, constants.ZERO_AMOUNT, - constants.NULL_ADDRESS, + constants.NULL_ERC20_ASSET_DATA, constants.ZERO_AMOUNT, - constants.NULL_ADDRESS, + constants.NULL_ERC20_ASSET_DATA, constants.NULL_ADDRESS, ); const BASE_TEST_SIGNED_ORDER: SignedOrder = { |