From 64e3b6f5eeb34c17d1c84d852268faac28b6ae86 Mon Sep 17 00:00:00 2001 From: David Sun Date: Fri, 31 May 2019 13:47:38 -0700 Subject: [PATCH] refactored ERC <> ERC for asset-buyer minor update to interface --- packages/asset-buyer/package.json | 2 +- packages/asset-buyer/src/asset_buyer.ts | 277 +++++++++++------- packages/asset-buyer/src/index.ts | 4 + .../order_providers/basic_order_provider.ts | 9 + .../standard_relayer_api_order_provider.ts | 28 ++ packages/asset-buyer/src/types.ts | 22 +- packages/asset-buyer/src/utils/assert.ts | 20 +- .../src/utils/buy_quote_calculator.ts | 90 +++--- .../src/utils/calculate_liquidity.ts | 18 +- packages/asset-buyer/test/utils/mocks.ts | 4 + 10 files changed, 287 insertions(+), 187 deletions(-) diff --git a/packages/asset-buyer/package.json b/packages/asset-buyer/package.json index d86c6aeff6..690e0a75b6 100644 --- a/packages/asset-buyer/package.json +++ b/packages/asset-buyer/package.json @@ -4,7 +4,7 @@ "engines": { "node": ">=6.12" }, - "description": "Convenience package for discovering and buying assets with Ether.", + "description": "Convenience package for discovering and buying assets for both on chain and off chain needs.", "main": "lib/src/index.js", "types": "lib/src/index.d.ts", "scripts": { diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index eb9c1f2036..b0c951d035 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -124,39 +124,39 @@ export class AssetBuyer { * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. */ public async getBuyQuoteAsync( - assetData: string, - assetBuyAmount: BigNumber, + makerAssetData: string, + takerAssetData: string, + makerAssetBuyAmount: BigNumber, options: Partial = {}, ): Promise { - const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = _.merge( + const { shouldForceOrderRefresh, slippagePercentage } = _.merge( {}, constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS, options, ); - assert.isString('assetData', assetData); - assert.isBigNumber('assetBuyAmount', assetBuyAmount); - assert.isValidPercentage('feePercentage', feePercentage); + assert.isString('makerAssetData', makerAssetData); + assert.isString('takerAssetData', takerAssetData); + assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount); assert.isBoolean('shouldForceOrderRefresh', shouldForceOrderRefresh); assert.isNumber('slippagePercentage', slippagePercentage); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); - const isMakerAssetZrxToken = assetData === zrxTokenAssetData; + const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData; // get the relevant orders for the makerAsset and fees // if the requested assetData is ZRX, don't get the fee info const [ordersAndFillableAmounts, feeOrdersAndFillableAmounts] = await Promise.all([ - this.getOrdersAndFillableAmountsAsync(assetData, shouldForceOrderRefresh), + this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData, shouldForceOrderRefresh), isMakerAssetZrxToken ? Promise.resolve(constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS) - : this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, shouldForceOrderRefresh), + : this.getOrdersAndFillableAmountsAsync(zrxTokenAssetData, takerAssetData, shouldForceOrderRefresh), shouldForceOrderRefresh, ]); if (ordersAndFillableAmounts.orders.length === 0) { - throw new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${assetData}`); + throw new Error(`${AssetBuyerError.AssetUnavailable}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`); } const buyQuote = buyQuoteCalculator.calculate( ordersAndFillableAmounts, feeOrdersAndFillableAmounts, - assetBuyAmount, - feePercentage, + makerAssetBuyAmount, slippagePercentage, isMakerAssetZrxToken, ); @@ -172,14 +172,17 @@ export class AssetBuyer { * @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information. */ public async getBuyQuoteForERC20TokenAddressAsync( - tokenAddress: string, - assetBuyAmount: BigNumber, + makerTokenAddress: string, + takerTokenAddress: string, + makerAssetBuyAmount: BigNumber, options: Partial = {}, ): Promise { - assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isBigNumber('assetBuyAmount', assetBuyAmount); - const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress); - const buyQuote = this.getBuyQuoteAsync(assetData, assetBuyAmount, options); + assert.isETHAddressHex('makerTokenAddress', makerTokenAddress); + assert.isETHAddressHex('takerTokenAddress', takerTokenAddress); + assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount); + const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); + const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); + const buyQuote = this.getBuyQuoteAsync(makerAssetData, takerAssetData, makerAssetBuyAmount, options); return buyQuote; } /** @@ -190,125 +193,162 @@ export class AssetBuyer { * * @return An object that conforms to LiquidityForAssetData that satisfies the request. See type definition for more information. */ - public async getLiquidityForAssetDataAsync( - assetData: string, + public async getLiquidityForMakerTakerAssetdataPairAsync( + makerAssetData: string, + takerAssetData: string, options: Partial = {}, ): Promise { const shouldForceOrderRefresh = options.shouldForceOrderRefresh !== undefined ? options.shouldForceOrderRefresh : false; - assert.isString('assetData', assetData); - assetDataUtils.decodeAssetDataOrThrow(assetData); + assert.isString('makerAssetDataa', makerAssetData); + assert.isString('takerAssetData', takerAssetData); + assetDataUtils.decodeAssetDataOrThrow(makerAssetData); + assetDataUtils.decodeAssetDataOrThrow(takerAssetData); assert.isBoolean('options.shouldForceOrderRefresh', shouldForceOrderRefresh); - const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(assetData); - const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); - if (!assetPairs.includes(etherTokenAssetData)) { + const assetPairs = await this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData); + if (!assetPairs.includes(makerAssetData)) { return { - tokensAvailableInBaseUnits: new BigNumber(0), - ethValueAvailableInWei: new BigNumber(0), + makerTokensAvailableInBaseUnits: new BigNumber(0), + takerTokensAvailableInBaseUnits: new BigNumber(0), }; } const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync( - assetData, + makerAssetData, + takerAssetData, shouldForceOrderRefresh, ); return calculateLiquidity(ordersAndFillableAmounts); } + // /** + // * Given a BuyQuote and desired rate, attempt to execute the buy. + // * @param buyQuote An object that conforms to BuyQuote. See type definition for more information. + // * @param options Options for the execution of the BuyQuote. See type definition for more information. + // * + // * @return A promise of the txHash. + // */ + // public async executeBuyQuoteAsync( + // buyQuote: BuyQuote, + // options: Partial = {}, + // ): Promise { + // const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = _.merge( + // {}, + // constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS, + // options, + // ); + // assert.isValidBuyQuote('buyQuote', buyQuote); + // if (ethAmount !== undefined) { + // assert.isBigNumber('ethAmount', ethAmount); + // } + // if (takerAddress !== undefined) { + // assert.isETHAddressHex('takerAddress', takerAddress); + // } + // assert.isETHAddressHex('feeRecipient', feeRecipient); + // if (gasLimit !== undefined) { + // assert.isNumber('gasLimit', gasLimit); + // } + // if (gasPrice !== undefined) { + // assert.isBigNumber('gasPrice', gasPrice); + // } + // const { orders, feeOrders, makerAssetBuyAmount, worstCaseQuoteInfo } = buyQuote; + // // TODO(dave4506) upgrade logic for asset-buyer2.0 + // // if no takerAddress is provided, try to get one from the provider + // // let finalTakerAddress; + // // if (takerAddress !== undefined) { + // // finalTakerAddress = takerAddress; + // // } else { + // // const web3Wrapper = new Web3Wrapper(this.provider); + // // const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + // // const firstAvailableAddress = _.head(availableAddresses); + // // if (firstAvailableAddress !== undefined) { + // // finalTakerAddress = firstAvailableAddress; + // // } else { + // // throw new Error(AssetBuyerError.NoAddressAvailable); + // // } + // // } + // // try { + // // // if no ethAmount is provided, default to the worst ethAmount from buyQuote + // // const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + // // orders, + // // assetBuyAmount, + // // finalTakerAddress, + // // ethAmount || worstCaseQuoteInfo.totalEthAmount, + // // feeOrders, + // // feePercentage, + // // feeRecipient, + // // { + // // gasLimit, + // // gasPrice, + // // shouldValidate: true, + // // }, + // // ); + // // return txHash; + // // } catch (err) { + // // if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) { + // // throw new Error(AssetBuyerError.SignatureRequestDenied); + // // } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) { + // // throw new Error(AssetBuyerError.TransactionValueTooLow); + // // } else { + // // throw err; + // // } + // // } + // return Promise.resolve('test'); + // } + /** - * Given a BuyQuote and desired rate, attempt to execute the buy. - * @param buyQuote An object that conforms to BuyQuote. See type definition for more information. - * @param options Options for the execution of the BuyQuote. See type definition for more information. - * - * @return A promise of the txHash. - */ - public async executeBuyQuoteAsync( - buyQuote: BuyQuote, - options: Partial = {}, - ): Promise { - const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = _.merge( - {}, - constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS, - options, - ); - assert.isValidBuyQuote('buyQuote', buyQuote); - if (ethAmount !== undefined) { - assert.isBigNumber('ethAmount', ethAmount); - } - if (takerAddress !== undefined) { - assert.isETHAddressHex('takerAddress', takerAddress); - } - assert.isETHAddressHex('feeRecipient', feeRecipient); - if (gasLimit !== undefined) { - assert.isNumber('gasLimit', gasLimit); - } - if (gasPrice !== undefined) { - assert.isBigNumber('gasPrice', gasPrice); - } - const { orders, feeOrders, feePercentage, assetBuyAmount, worstCaseQuoteInfo } = buyQuote; - // if no takerAddress is provided, try to get one from the provider - let finalTakerAddress; - if (takerAddress !== undefined) { - finalTakerAddress = takerAddress; - } else { - const web3Wrapper = new Web3Wrapper(this.provider); - const availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); - const firstAvailableAddress = _.head(availableAddresses); - if (firstAvailableAddress !== undefined) { - finalTakerAddress = firstAvailableAddress; - } else { - throw new Error(AssetBuyerError.NoAddressAvailable); - } - } - try { - // if no ethAmount is provided, default to the worst ethAmount from buyQuote - const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( - orders, - assetBuyAmount, - finalTakerAddress, - ethAmount || worstCaseQuoteInfo.totalEthAmount, - feeOrders, - feePercentage, - feeRecipient, - { - gasLimit, - gasPrice, - shouldValidate: true, - }, - ); - return txHash; - } catch (err) { - if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) { - throw new Error(AssetBuyerError.SignatureRequestDenied); - } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) { - throw new Error(AssetBuyerError.TransactionValueTooLow); - } else { - throw err; - } - } - } - /** - * Get the asset data of all assets that are purchaseable with ether token (wETH) in the order provider passed in at init. + * Get the asset data of all assets that can be used to purchase makerAssetData in the order provider passed in at init. * * @return An array of asset data strings that can be purchased using wETH. */ - public async getAvailableAssetDatasAsync(): Promise { - const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); - return this.orderProvider.getAvailableMakerAssetDatasAsync(etherTokenAssetData); + public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise { + assert.isString('makerAssetDataa', makerAssetData); + assetDataUtils.decodeAssetDataOrThrow(makerAssetData); + return this.orderProvider.getAvailableTakerAssetDatasAsync(makerAssetData); } + + /** + * Get the asset data of all assets that are purchaseable with makerAssetData in the order provider passed in at init. + * + * @return An array of asset data strings that can be purchased using wETH. + */ + public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise { + assert.isString('takerAssetData', takerAssetData); + assetDataUtils.decodeAssetDataOrThrow(takerAssetData); + return this.orderProvider.getAvailableMakerAssetDatasAsync(takerAssetData); + } + + /** + * validates that the taker + maker asset pair is availalbe from the order provider passed + * + * @return A boolean on if the taker + maker pair exists + */ + public async isTakerMakerAssetDataPairAvailableAsync(makerAssetData: string, takerAssetData: string): Promise { + assert.isString('makerAssetDataa', makerAssetData); + assert.isString('takerAssetData', takerAssetData); + assetDataUtils.decodeAssetDataOrThrow(makerAssetData); + assetDataUtils.decodeAssetDataOrThrow(takerAssetData); + return _.findIndex(await this.getAvailableMakerAssetDatasAsync(takerAssetData), makerAssetData) !== -1; + } + /** * Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders * @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * @param shouldForceOrderRefresh If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. */ public async getOrdersAndFillableAmountsAsync( - assetData: string, + makerAssetData: string, + takerAssetData: string, shouldForceOrderRefresh: boolean, ): Promise { + assert.isString('makerAssetDataa', makerAssetData); + assert.isString('takerAssetData', takerAssetData); + assetDataUtils.decodeAssetDataOrThrow(makerAssetData); + assetDataUtils.decodeAssetDataOrThrow(takerAssetData); // try to get ordersEntry from the map - const ordersEntryIfExists = this._ordersEntryMap[assetData]; + const ordersEntryIfExists = this._ordersEntryMap[this._getOrdersEntryMapKey(makerAssetData, takerAssetData)]; // we should refresh if: // we do not have any orders OR // we are forced to OR @@ -322,12 +362,11 @@ export class AssetBuyer { const result = ordersEntryIfExists.ordersAndFillableAmounts; return result; } - const etherTokenAssetData = this._getEtherTokenAssetDataOrThrow(); const zrxTokenAssetData = this._getZrxTokenAssetDataOrThrow(); // construct orderProvider request const orderProviderRequest = { - makerAssetData: assetData, - takerAssetData: etherTokenAssetData, + makerAssetData, + takerAssetData, networkId: this.networkId, }; const request = orderProviderRequest; @@ -337,7 +376,7 @@ export class AssetBuyer { // ie. it should only return maker/taker assetDatas that are specified orderProviderResponseProcessor.throwIfInvalidResponse(response, request); // process the responses into one object - const isMakerAssetZrxToken = assetData === zrxTokenAssetData; + const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData; const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync( response, isMakerAssetZrxToken, @@ -349,16 +388,26 @@ export class AssetBuyer { ordersAndFillableAmounts, lastRefreshTime, }; - this._ordersEntryMap[assetData] = updatedOrdersEntry; + this._ordersEntryMap[this._getOrdersEntryMapKey(makerAssetData, takerAssetData)] = updatedOrdersEntry; return ordersAndFillableAmounts; } + + /** + * + * get the key for _orderEntryMap for maker + taker asset pair + */ + // tslint:disable-next-line: prefer-function-over-method + private _getOrdersEntryMapKey(makerAssetData: string, takerAssetData: string): string { + return `${makerAssetData}_${takerAssetData}`; + } + /** * Get the assetData that represents the WETH token. * Will throw if WETH does not exist for the current network. */ - private _getEtherTokenAssetDataOrThrow(): string { - return assetDataUtils.getEtherTokenAssetData(this._contractWrappers); - } + // private _getEtherTokenAssetDataOrThrow(): string { + // return assetDataUtils.getEtherTokenAssetData(this._contractWrappers); + // } /** * Get the assetData that represents the ZRX token. * Will throw if ZRX does not exist for the current network. diff --git a/packages/asset-buyer/src/index.ts b/packages/asset-buyer/src/index.ts index 78ce978276..9006dab307 100644 --- a/packages/asset-buyer/src/index.ts +++ b/packages/asset-buyer/src/index.ts @@ -13,13 +13,17 @@ export { Web3JsV2Provider, Web3JsV3Provider, } from 'ethereum-types'; + +// TODO(dave4506): if this lives under the 0x.js library, then these type exports should be removed in favor of minimizing redundancy export { SignedOrder } from '@0x/types'; export { BigNumber } from '@0x/utils'; export { AssetBuyer } from './asset_buyer'; export { InsufficientAssetLiquidityError } from './errors'; + export { BasicOrderProvider } from './order_providers/basic_order_provider'; export { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider'; + export { AssetBuyerError, AssetBuyerOpts, diff --git a/packages/asset-buyer/src/order_providers/basic_order_provider.ts b/packages/asset-buyer/src/order_providers/basic_order_provider.ts index 76685f27af..647f176afd 100644 --- a/packages/asset-buyer/src/order_providers/basic_order_provider.ts +++ b/packages/asset-buyer/src/order_providers/basic_order_provider.ts @@ -38,4 +38,13 @@ export class BasicOrderProvider implements OrderProvider { const ordersWithTakerAssetData = _.filter(this.orders, { takerAssetData }); return _.map(ordersWithTakerAssetData, order => order.makerAssetData); } + /** + * Given a maker asset data string, return all availabled paired taker asset data strings. + * @param makerAssetData A string representing the maker asset data. + * @return An array of asset data strings that can be used to purchased makerAssetData. + */ + public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise { + const ordersWithMakerAssetData = _.filter(this.orders, { makerAssetData }); + return _.map(ordersWithMakerAssetData, order => order.takerAssetData); + } } diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts index abb9544e7e..10b631431c 100644 --- a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts +++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts @@ -111,4 +111,32 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider { } }); } + /** + * Given a maker asset data string, return all availabled paired taker asset data strings. + * @param makerAssetData A string representing the maker asset data. + * @return An array of asset data strings that can be used to purchased makerAssetData. + */ + public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise { + // Return a maximum of 1000 asset datas + const maxPerPage = 1000; + const requestOpts = { networkId: this.networkId, perPage: maxPerPage }; + const assetPairsRequest = { assetDataA: makerAssetData }; + const fullRequest = { + ...requestOpts, + ...assetPairsRequest, + }; + let response: AssetPairsResponse; + try { + response = await this._sraClient.getAssetPairsAsync(fullRequest); + } catch (err) { + throw new Error(AssetBuyerError.StandardRelayerApiError); + } + return _.map(response.records, item => { + if (item.assetDataA.assetData === makerAssetData) { + return item.assetDataB.assetData; + } else { + return item.assetDataA.assetData; + } + }); + } } diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 46a2338cea..40d9d7bab0 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -25,6 +25,7 @@ export interface OrderProviderResponse { export interface SignedOrderWithRemainingFillableMakerAssetAmount extends SignedOrder { remainingFillableMakerAssetAmount?: BigNumber; } + /** * gerOrdersAsync: Given an OrderProviderRequest, get an OrderProviderResponse. * getAvailableMakerAssetDatasAsync: Given a taker asset data string, return all availabled paired maker asset data strings. @@ -32,6 +33,7 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed export interface OrderProvider { getOrdersAsync: (orderProviderRequest: OrderProviderRequest) => Promise; getAvailableMakerAssetDatasAsync: (takerAssetData: string) => Promise; + getAvailableTakerAssetDatasAsync: (makerAssetData: string) => Promise; } /** @@ -44,13 +46,15 @@ export interface OrderProvider { * worstCaseQuoteInfo: Info about the worst case price for the asset. */ export interface BuyQuote { - assetData: string; - assetBuyAmount: BigNumber; + takerAssetData: string; + makerAssetData: string; + makerAssetBuyAmount: BigNumber; orders: SignedOrder[]; feeOrders: SignedOrder[]; - feePercentage?: number; bestCaseQuoteInfo: BuyQuoteInfo; worstCaseQuoteInfo: BuyQuoteInfo; + toAddress: string; // exchange address, coordinator address + isUsingCoordinator: boolean; } /** @@ -59,18 +63,16 @@ export interface BuyQuote { * totalEthAmount: The total amount of eth required to complete the buy (filling orders, feeOrders, and paying affiliate fee). */ export interface BuyQuoteInfo { - assetEthAmount: BigNumber; - feeEthAmount: BigNumber; - totalEthAmount: BigNumber; + takerTokenAmount: BigNumber; + feeTakerTokenAmount: BigNumber; + totalTakerTokenAmount: BigNumber; } /** - * feePercentage: The affiliate fee percentage. Defaults to 0. * shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false. * slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%). */ export interface BuyQuoteRequestOpts { - feePercentage: number; shouldForceOrderRefresh: boolean; slippagePercentage: number; } @@ -137,6 +139,6 @@ export interface OrdersAndFillableAmounts { * Represents available liquidity for a given assetData */ export interface LiquidityForAssetData { - tokensAvailableInBaseUnits: BigNumber; - ethValueAvailableInWei: BigNumber; + makerTokensAvailableInBaseUnits: BigNumber; + takerTokensAvailableInBaseUnits: BigNumber; } diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 464ecdc3fb..7bbceeaf82 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -6,20 +6,24 @@ import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../ export const assert = { ...sharedAssert, isValidBuyQuote(variableName: string, buyQuote: BuyQuote): void { - sharedAssert.isHexString(`${variableName}.assetData`, buyQuote.assetData); + sharedAssert.isHexString(`${variableName}.takerAssetData`, buyQuote.takerAssetData); + sharedAssert.isHexString(`${variableName}.makerAssetData`, buyQuote.makerAssetData); sharedAssert.doesConformToSchema(`${variableName}.orders`, buyQuote.orders, schemas.signedOrdersSchema); sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, buyQuote.feeOrders, schemas.signedOrdersSchema); assert.isValidBuyQuoteInfo(`${variableName}.bestCaseQuoteInfo`, buyQuote.bestCaseQuoteInfo); assert.isValidBuyQuoteInfo(`${variableName}.worstCaseQuoteInfo`, buyQuote.worstCaseQuoteInfo); - sharedAssert.isBigNumber(`${variableName}.assetBuyAmount`, buyQuote.assetBuyAmount); - if (buyQuote.feePercentage !== undefined) { - sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); - } + sharedAssert.isBigNumber(`${variableName}.makerAssetBuyAmount`, buyQuote.makerAssetBuyAmount); + assert.isETHAddressHex(`${variableName}.toAddress`, buyQuote.toAddress); + assert.isBoolean(`${variableName}.isUsingCoordinator`, buyQuote.isUsingCoordinator); + // TODO(dave4506) Remove once forwarder features are reimplemented + // if (buyQuote.feePercentage !== undefined) { + // sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); + // } }, isValidBuyQuoteInfo(variableName: string, buyQuoteInfo: BuyQuoteInfo): void { - sharedAssert.isBigNumber(`${variableName}.assetEthAmount`, buyQuoteInfo.assetEthAmount); - sharedAssert.isBigNumber(`${variableName}.feeEthAmount`, buyQuoteInfo.feeEthAmount); - sharedAssert.isBigNumber(`${variableName}.totalEthAmount`, buyQuoteInfo.totalEthAmount); + sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, buyQuoteInfo.takerTokenAmount); + sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, buyQuoteInfo.feeTakerTokenAmount); + sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, buyQuoteInfo.totalTakerTokenAmount); }, isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void { sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync); diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts index 68dec0e611..875fbfc9d9 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -11,8 +11,7 @@ export const buyQuoteCalculator = { calculate( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, - assetBuyAmount: BigNumber, - feePercentage: number, + makerAssetBuyAmount: BigNumber, slippagePercentage: number, isMakerAssetZrxToken: boolean, ): BuyQuote { @@ -20,20 +19,20 @@ export const buyQuoteCalculator = { const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; const feeOrders = feeOrdersAndFillableAmounts.orders; const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts; - const slippageBufferAmount = assetBuyAmount.multipliedBy(slippagePercentage).integerValue(); + const slippageBufferAmount = makerAssetBuyAmount.multipliedBy(slippagePercentage).integerValue(); // find the orders that cover the desired assetBuyAmount (with slippage) const { resultOrders, remainingFillAmount, ordersRemainingFillableMakerAssetAmounts, - } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetBuyAmount, { + } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, makerAssetBuyAmount, { remainingFillableMakerAssetAmounts, slippageBufferAmount, }); // if we do not have enough orders to cover the desired assetBuyAmount, throw if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) { // We needed the amount they requested to buy, plus the amount for slippage - const totalAmountRequested = assetBuyAmount.plus(slippageBufferAmount); + const totalAmountRequested = makerAssetBuyAmount.plus(slippageBufferAmount); const amountAbleToFill = totalAmountRequested.minus(remainingFillAmount); // multiplierNeededWithSlippage represents what we need to multiply the assetBuyAmount by // in order to get the total amount needed considering slippage @@ -73,7 +72,9 @@ export const buyQuoteCalculator = { } // assetData information for the result - const assetData = orders[0].makerAssetData; + const takerAssetData = orders[0].takerAssetData; + const makerAssetData = orders[0].makerAssetData; + // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = { orders: resultOrders, @@ -86,26 +87,28 @@ export const buyQuoteCalculator = { const bestCaseQuoteInfo = calculateQuoteInfo( trimmedOrdersAndFillableAmounts, trimmedFeeOrdersAndFillableAmounts, - assetBuyAmount, - feePercentage, + makerAssetBuyAmount, isMakerAssetZrxToken, ); // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate const worstCaseQuoteInfo = calculateQuoteInfo( reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts), reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts), - assetBuyAmount, - feePercentage, + makerAssetBuyAmount, isMakerAssetZrxToken, ); + return { - assetData, + takerAssetData, + makerAssetData, + makerAssetBuyAmount, orders: resultOrders, feeOrders: resultFeeOrders, bestCaseQuoteInfo, worstCaseQuoteInfo, - assetBuyAmount, - feePercentage, + // TODO(dave4506): coordinator metadata for buy quote + toAddress: constants.NULL_ADDRESS, + isUsingCoordinator: false, }; }, }; @@ -113,33 +116,29 @@ export const buyQuoteCalculator = { function calculateQuoteInfo( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, - assetBuyAmount: BigNumber, - feePercentage: number, + makserAssetBuyAmount: BigNumber, isMakerAssetZrxToken: boolean, ): BuyQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right - let assetEthAmount = constants.ZERO_AMOUNT; - let zrxEthAmount = constants.ZERO_AMOUNT; + let takerTokenAmount = constants.ZERO_AMOUNT; + let zrxTakerTokenAmount = constants.ZERO_AMOUNT; if (isMakerAssetZrxToken) { - assetEthAmount = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount); + takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makserAssetBuyAmount); } else { // find eth and zrx amounts needed to buy - const ethAndZrxAmountToBuyAsset = findEthAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, assetBuyAmount); - assetEthAmount = ethAndZrxAmountToBuyAsset[0]; - const zrxAmountToBuyAsset = ethAndZrxAmountToBuyAsset[1]; + const takerTokenAndZrxAmountToBuyAsset = findTakerTokenAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, makserAssetBuyAmount); + takerTokenAmount = takerTokenAndZrxAmountToBuyAsset[0]; + const zrxAmountToBuyAsset = takerTokenAndZrxAmountToBuyAsset[1]; // find eth amount needed to buy zrx - zrxEthAmount = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); + zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); } - // eth amount needed to buy the affiliate fee - const affiliateFeeEthAmount = assetEthAmount.multipliedBy(feePercentage).integerValue(BigNumber.ROUND_CEIL); - // eth amount needed for fees is the sum of affiliate fee and zrx fee - const feeEthAmount = affiliateFeeEthAmount.plus(zrxEthAmount); + const feeTakerTokenAmount = zrxTakerTokenAmount; // eth amount needed in total is the sum of the amount needed for the asset and the amount needed for fees - const totalEthAmount = assetEthAmount.plus(feeEthAmount); + const totalTakerTokenAmount = takerTokenAmount.plus(feeTakerTokenAmount); return { - assetEthAmount, - feeEthAmount, - totalEthAmount, + takerTokenAmount, + feeTakerTokenAmount, + totalTakerTokenAmount, }; } @@ -153,7 +152,7 @@ function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFill }; } -function findEthAmountNeededToBuyZrx( +function findTakerTokenAmountNeededToBuyZrx( feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, zrxBuyAmount: BigNumber, ): BigNumber { @@ -161,18 +160,19 @@ function findEthAmountNeededToBuyZrx( const result = _.reduce( orders, (acc, order, index) => { - const { totalEthAmount, remainingZrxBuyAmount } = acc; + const { totalTakerTokenAmount, remainingZrxBuyAmount } = acc; const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; const makerFillAmount = BigNumber.min(remainingZrxBuyAmount, remainingFillableMakerAssetAmount); const [takerFillAmount, adjustedMakerFillAmount] = orderCalculationUtils.getTakerFillAmountForFeeOrder( order, makerFillAmount, ); + // TODO(dave4506) may remove if this is for affiliate fees (asset-buyer2.0) const extraFeeAmount = remainingFillableMakerAssetAmount.isGreaterThanOrEqualTo(adjustedMakerFillAmount) ? constants.ZERO_AMOUNT : adjustedMakerFillAmount.minus(makerFillAmount); return { - totalEthAmount: totalEthAmount.plus(takerFillAmount), + totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount), remainingZrxBuyAmount: BigNumber.max( constants.ZERO_AMOUNT, remainingZrxBuyAmount.minus(makerFillAmount).plus(extraFeeAmount), @@ -180,40 +180,40 @@ function findEthAmountNeededToBuyZrx( }; }, { - totalEthAmount: constants.ZERO_AMOUNT, + totalTakerTokenAmount: constants.ZERO_AMOUNT, remainingZrxBuyAmount: zrxBuyAmount, }, ); - return result.totalEthAmount; + return result.totalTakerTokenAmount; } -function findEthAndZrxAmountNeededToBuyAsset( +function findTakerTokenAndZrxAmountNeededToBuyAsset( ordersAndFillableAmounts: OrdersAndFillableAmounts, - assetBuyAmount: BigNumber, + makerAssetBuyAmount: BigNumber, ): [BigNumber, BigNumber] { const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts; const result = _.reduce( orders, (acc, order, index) => { - const { totalEthAmount, totalZrxAmount, remainingAssetBuyAmount } = acc; + const { totalTakerTokenAmount, totalZrxAmount, remainingMakerAssetBuyAmount } = acc; const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; - const makerFillAmount = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount); + const makerFillAmount = BigNumber.min(acc.remainingMakerAssetBuyAmount, remainingFillableMakerAssetAmount); const takerFillAmount = orderCalculationUtils.getTakerFillAmount(order, makerFillAmount); const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount); return { - totalEthAmount: totalEthAmount.plus(takerFillAmount), + totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount), totalZrxAmount: totalZrxAmount.plus(takerFeeAmount), - remainingAssetBuyAmount: BigNumber.max( + remainingMakerAssetBuyAmount: BigNumber.max( constants.ZERO_AMOUNT, - remainingAssetBuyAmount.minus(makerFillAmount), + remainingMakerAssetBuyAmount.minus(makerFillAmount), ), }; }, { - totalEthAmount: constants.ZERO_AMOUNT, + totalTakerTokenAmount: constants.ZERO_AMOUNT, totalZrxAmount: constants.ZERO_AMOUNT, - remainingAssetBuyAmount: assetBuyAmount, + remainingMakerAssetBuyAmount: makerAssetBuyAmount, }, ); - return [result.totalEthAmount, result.totalZrxAmount]; + return [result.totalTakerTokenAmount, result.totalZrxAmount]; } diff --git a/packages/asset-buyer/src/utils/calculate_liquidity.ts b/packages/asset-buyer/src/utils/calculate_liquidity.ts index 1b40e27dba..1500871045 100644 --- a/packages/asset-buyer/src/utils/calculate_liquidity.ts +++ b/packages/asset-buyer/src/utils/calculate_liquidity.ts @@ -12,25 +12,25 @@ export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAm throw new Error(`No corresponding fillableMakerAssetAmounts at index ${curIndex}`); } - const tokensAvailableForCurrentOrder = availableMakerAssetAmount; - const ethValueAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount( + const makerTokensAvailableForCurrentOrder = availableMakerAssetAmount; + const takerTokensAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount( order, - availableMakerAssetAmount, + makerTokensAvailableForCurrentOrder, ); return { - tokensAvailableInBaseUnits: acc.tokensAvailableInBaseUnits.plus(tokensAvailableForCurrentOrder), - ethValueAvailableInWei: acc.ethValueAvailableInWei.plus(ethValueAvailableForCurrentOrder), + makerTokensAvailableInBaseUnits: acc.makerTokensAvailableInBaseUnits.plus(makerTokensAvailableForCurrentOrder), + takerTokensAvailableInBaseUnits: acc.takerTokensAvailableInBaseUnits.plus(takerTokensAvailableForCurrentOrder), }; }, { - tokensAvailableInBaseUnits: new BigNumber(0), - ethValueAvailableInWei: new BigNumber(0), + makerTokensAvailableInBaseUnits: new BigNumber(0), + takerTokensAvailableInBaseUnits: new BigNumber(0), }, ); // Turn into regular numbers return { - tokensAvailableInBaseUnits: liquidityInBigNumbers.tokensAvailableInBaseUnits, - ethValueAvailableInWei: liquidityInBigNumbers.ethValueAvailableInWei, + makerTokensAvailableInBaseUnits: liquidityInBigNumbers.makerTokensAvailableInBaseUnits, + takerTokensAvailableInBaseUnits: liquidityInBigNumbers.takerTokensAvailableInBaseUnits, }; }; diff --git a/packages/asset-buyer/test/utils/mocks.ts b/packages/asset-buyer/test/utils/mocks.ts index d3e1c09c44..917f4fee4a 100644 --- a/packages/asset-buyer/test/utils/mocks.ts +++ b/packages/asset-buyer/test/utils/mocks.ts @@ -16,6 +16,10 @@ class OrderProviderClass implements OrderProvider { public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise { return Promise.resolve([]); } + // tslint:disable-next-line:prefer-function-over-method + public async getAvailableTakerAssetDatasAsync(takerAssetData: string): Promise { + return Promise.resolve([]); + } } export const orderProviderMock = () => {