diff options
Diffstat (limited to 'packages/order-utils/src')
-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/store/balance_and_proxy_allowance_lazy_store.ts | 2 |
6 files changed, 400 insertions, 86 deletions
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/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 && |