From 63d97f4c8837dcf8d17afc494dcd8b1ba10fee16 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 4 Oct 2018 14:33:07 -0700 Subject: Update BuyQuote interface --- packages/asset-buyer/src/asset_buyer.ts | 15 ++++------ packages/asset-buyer/src/types.ts | 27 ++++++++++++----- packages/asset-buyer/src/utils/assert.ts | 11 +++++-- .../asset-buyer/src/utils/buy_quote_calculator.ts | 26 ++++++++++------- .../asset-buyer/test/buy_quote_calculator_test.ts | 34 +++++++++++++++------- 5 files changed, 73 insertions(+), 40 deletions(-) diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index a56bc01d5..7ec39e012 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -183,19 +183,19 @@ export class AssetBuyer { buyQuote: BuyQuote, options: Partial = {}, ): Promise { - const { rate, takerAddress, feeRecipient } = { + const { ethAmount, takerAddress, feeRecipient } = { ...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS, ...options, }; assert.isValidBuyQuote('buyQuote', buyQuote); - if (!_.isUndefined(rate)) { - assert.isBigNumber('rate', rate); + if (!_.isUndefined(ethAmount)) { + assert.isBigNumber('ethAmount', ethAmount); } if (!_.isUndefined(takerAddress)) { assert.isETHAddressHex('takerAddress', takerAddress); } assert.isETHAddressHex('feeRecipient', feeRecipient); - const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = buyQuote; + const { orders, feeOrders, feePercentage, assetBuyAmount, worstCaseQuoteInfo } = buyQuote; // if no takerAddress is provided, try to get one from the provider let finalTakerAddress; if (!_.isUndefined(takerAddress)) { @@ -210,15 +210,12 @@ export class AssetBuyer { throw new Error(AssetBuyerError.NoAddressAvailable); } } - // if no rate is provided, default to the maxRate from buyQuote - const desiredRate = rate || maxRate; - // calculate how much eth is required to buy assetBuyAmount at the desired rate - const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate); + // if no ethAmount is provided, default to the worst ethAmount from buyQuote const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( orders, assetBuyAmount, finalTakerAddress, - ethAmount, + ethAmount || worstCaseQuoteInfo.totalEthAmount, feeOrders, feePercentage, feeRecipient, diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 8d3dcbfe6..b96795bb6 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -35,21 +35,32 @@ export interface OrderProvider { /** * assetData: String that represents a specific asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). + * assetBuyAmount: The amount of asset to buy. * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. * feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above. - * minRate: Min rate that needs to be paid in order to execute the buy. - * maxRate: Max rate that can be paid in order to execute the buy. - * assetBuyAmount: The amount of asset to buy. * feePercentage: Optional affiliate fee percentage used to calculate the eth amounts above. + * bestCaseQuoteInfo: Info about the best case price for the asset. + * worstCaseQuoteInfo: Info about the worst case price for the asset. */ export interface BuyQuote { assetData: string; + assetBuyAmount: BigNumber; orders: SignedOrder[]; feeOrders: SignedOrder[]; - minRate: BigNumber; - maxRate: BigNumber; - assetBuyAmount: BigNumber; feePercentage?: number; + bestCaseQuoteInfo: BuyQuoteInfo; + worstCaseQuoteInfo: BuyQuoteInfo; +} + +/** + * ethPerAssetPrice: The price of one unit of the desired asset in ETH + * feeEthAmount: The amount of eth required to pay the affiliate fee. + * totalEthAmount: the total amount of eth required to complete the buy. (Filling orders, feeOrders, and paying affiliate fee) + */ +export interface BuyQuoteInfo { + ethPerAssetPrice: BigNumber; + feeEthAmount: BigNumber; + totalEthAmount: BigNumber; } /** @@ -64,12 +75,12 @@ export interface BuyQuoteRequestOpts { } /** - * rate: The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate. + * ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount. * takerAddress: The address to perform the buy. Defaults to the first available address from the provider. * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000). */ export interface BuyQuoteExecutionOpts { - rate?: BigNumber; + ethAmount?: BigNumber; takerAddress?: string; feeRecipient: string; } diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index 04f425237..d43b71fee 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -3,7 +3,7 @@ import { schemas } from '@0xproject/json-schemas'; import { SignedOrder } from '@0xproject/types'; import * as _ from 'lodash'; -import { BuyQuote, OrderProvider, OrderProviderRequest } from '../types'; +import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../types'; export const assert = { ...sharedAssert, @@ -11,13 +11,18 @@ export const assert = { sharedAssert.isHexString(`${variableName}.assetData`, buyQuote.assetData); sharedAssert.doesConformToSchema(`${variableName}.orders`, buyQuote.orders, schemas.signedOrdersSchema); sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, buyQuote.feeOrders, schemas.signedOrdersSchema); - sharedAssert.isBigNumber(`${variableName}.minRate`, buyQuote.minRate); - sharedAssert.isBigNumber(`${variableName}.maxRate`, buyQuote.maxRate); + assert.isValidBuyQuoteInfo(`${variableName}.bestCaseQuoteInfo`, buyQuote.bestCaseQuoteInfo); + assert.isValidBuyQuoteInfo(`${variableName}.worstCaseQuoteInfo`, buyQuote.worstCaseQuoteInfo); sharedAssert.isBigNumber(`${variableName}.assetBuyAmount`, buyQuote.assetBuyAmount); if (!_.isUndefined(buyQuote.feePercentage)) { sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage); } }, + isValidBuyQuoteInfo(variableName: string, buyQuoteInfo: BuyQuoteInfo): void { + sharedAssert.isBigNumber(`${variableName}.ethPerAssetPrice`, buyQuoteInfo.ethPerAssetPrice); + sharedAssert.isBigNumber(`${variableName}.feeEthAmount`, buyQuoteInfo.feeEthAmount); + sharedAssert.isBigNumber(`${variableName}.totalEthAmount`, buyQuoteInfo.totalEthAmount); + }, 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 78666356c..cb0fd128c 100644 --- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts @@ -3,7 +3,7 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import { constants } from '../constants'; -import { AssetBuyerError, BuyQuote, OrdersAndFillableAmounts } from '../types'; +import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types'; // Calculates a buy quote for orders that have WETH as the takerAsset export const buyQuoteCalculator = { @@ -59,37 +59,38 @@ export const buyQuoteCalculator = { orders: resultFeeOrders, remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts, }; - const minRate = calculateRate( + const bestCaseQuoteInfo = calculateQuoteInfo( trimmedOrdersAndFillableAmounts, trimmedFeeOrdersAndFillableAmounts, assetBuyAmount, feePercentage, ); // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate - const maxRate = calculateRate( + const worstCaseQuoteInfo = calculateQuoteInfo( reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts), reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts), assetBuyAmount, feePercentage, ); + return { assetData, orders: resultOrders, feeOrders: resultFeeOrders, - minRate, - maxRate, + bestCaseQuoteInfo, + worstCaseQuoteInfo, assetBuyAmount, feePercentage, }; }, }; -function calculateRate( +function calculateQuoteInfo( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, assetBuyAmount: BigNumber, feePercentage: number, -): BigNumber { +): BuyQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right const [ethAmountToBuyAsset, zrxAmountToBuyAsset] = findEthAndZrxAmountNeededToBuyAsset( ordersAndFillableAmounts, @@ -97,10 +98,15 @@ function calculateRate( ); // find the total eth needed to buy fees const ethAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); - const ethAmount = ethAmountToBuyAsset.plus(ethAmountToBuyFees).mul(feePercentage + 1); + const ethAmountBeforeAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees); + const totalEthAmount = ethAmountBeforeAffiliateFee.mul(feePercentage + 1); // divide into the assetBuyAmount in order to find rate of makerAsset / WETH - const result = assetBuyAmount.div(ethAmount); - return result; + const ethPerAssetPrice = ethAmountBeforeAffiliateFee.div(assetBuyAmount); + return { + totalEthAmount, + feeEthAmount: totalEthAmount.minus(ethAmountBeforeAffiliateFee), + ethPerAssetPrice, + }; } // given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties diff --git a/packages/asset-buyer/test/buy_quote_calculator_test.ts b/packages/asset-buyer/test/buy_quote_calculator_test.ts index 667dec051..b987b45a8 100644 --- a/packages/asset-buyer/test/buy_quote_calculator_test.ts +++ b/packages/asset-buyer/test/buy_quote_calculator_test.ts @@ -103,11 +103,17 @@ describe('buyQuoteCalculator', () => { expect(buyQuote.feeOrders).to.deep.equal([smallFeeOrderAndFillableAmount.orders[0]]); // test if rates are correct // 50 eth to fill the first order + 100 eth for fees - const expectedMinEthToFill = new BigNumber(150); - const expectedMinRate = assetBuyAmount.div(expectedMinEthToFill.mul(feePercentage + 1)); - expect(buyQuote.minRate).to.bignumber.equal(expectedMinRate); + const expectedFillEthAmount = new BigNumber(150); + const expectedTotalEthAmount = expectedFillEthAmount.mul(feePercentage + 1); + const expectedFeeEthAmount = expectedTotalEthAmount.minus(expectedFillEthAmount); + const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount); + expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); + expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); + expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); // because we have no slippage protection, minRate is equal to maxRate - expect(buyQuote.maxRate).to.bignumber.equal(expectedMinRate); + expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); + expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); + expect(buyQuote.worstCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); // test if feePercentage gets passed through expect(buyQuote.feePercentage).to.equal(feePercentage); }); @@ -132,13 +138,21 @@ describe('buyQuoteCalculator', () => { expect(buyQuote.feeOrders).to.deep.equal(allFeeOrdersAndFillableAmounts.orders); // test if rates are correct // 50 eth to fill the first order + 100 eth for fees - const expectedMinEthToFill = new BigNumber(150); - const expectedMinRate = assetBuyAmount.div(expectedMinEthToFill.mul(feePercentage + 1)); - expect(buyQuote.minRate).to.bignumber.equal(expectedMinRate); + const expectedFillEthAmount = new BigNumber(150); + const expectedTotalEthAmount = expectedFillEthAmount.mul(feePercentage + 1); + const expectedFeeEthAmount = expectedTotalEthAmount.minus(expectedFillEthAmount); + const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount); + expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); + expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); + expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); // 100 eth to fill the first order + 200 eth for fees - const expectedMaxEthToFill = new BigNumber(300); - const expectedMaxRate = assetBuyAmount.div(expectedMaxEthToFill.mul(feePercentage + 1)); - expect(buyQuote.maxRate).to.bignumber.equal(expectedMaxRate); + const expectedWorstFillEthAmount = new BigNumber(300); + const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.mul(feePercentage + 1); + const expectedWorstFeeEthAmount = expectedWorstTotalEthAmount.minus(expectedWorstFillEthAmount); + const expectedWorstEthPerAssetPrice = expectedWorstFillEthAmount.div(assetBuyAmount); + expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedWorstFeeEthAmount); + expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedWorstTotalEthAmount); + expect(buyQuote.worstCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedWorstEthPerAssetPrice); // test if feePercentage gets passed through expect(buyQuote.feePercentage).to.equal(feePercentage); }); -- cgit v1.2.3