aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrandon Millman <brandon@0xproject.com>2018-10-05 05:49:25 +0800
committerGitHub <noreply@github.com>2018-10-05 05:49:25 +0800
commitab28e6af3507d7eea163e37730e68a101b525237 (patch)
treea08c4bf3cd54a34ecb76ac8e19a51228bf25da97
parent2da7626b5356f7b0ec809719f1b9da14b2969a07 (diff)
parent63d97f4c8837dcf8d17afc494dcd8b1ba10fee16 (diff)
downloaddexon-sol-tools-ab28e6af3507d7eea163e37730e68a101b525237.tar.gz
dexon-sol-tools-ab28e6af3507d7eea163e37730e68a101b525237.tar.zst
dexon-sol-tools-ab28e6af3507d7eea163e37730e68a101b525237.zip
Merge pull request #1113 from 0xProject/feature/asset-buyer/tests
[asset-buyer] Rework min / max rate calc and add tests
-rw-r--r--packages/asset-buyer/CHANGELOG.json4
-rw-r--r--packages/asset-buyer/src/asset_buyer.ts24
-rw-r--r--packages/asset-buyer/src/types.ts27
-rw-r--r--packages/asset-buyer/src/utils/assert.ts11
-rw-r--r--packages/asset-buyer/src/utils/buy_quote_calculator.ts159
-rw-r--r--packages/asset-buyer/test/buy_quote_calculator_test.ts160
-rw-r--r--packages/order-utils/src/order_factory.ts39
7 files changed, 366 insertions, 58 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json
index 8c2c7a8d2..df67059cb 100644
--- a/packages/asset-buyer/CHANGELOG.json
+++ b/packages/asset-buyer/CHANGELOG.json
@@ -5,6 +5,10 @@
{
"note": "Expand AssetBuyer to work with multiple assets at once",
"pr": 1086
+ },
+ {
+ "note": "Fix minRate and maxRate calculation",
+ "pr": 1113
}
]
},
diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts
index 0bb757f52..7ec39e012 100644
--- a/packages/asset-buyer/src/asset_buyer.ts
+++ b/packages/asset-buyer/src/asset_buyer.ts
@@ -123,7 +123,7 @@ export class AssetBuyer {
public async getBuyQuoteAsync(
assetData: string,
assetBuyAmount: BigNumber,
- options: Partial<BuyQuoteRequestOpts>,
+ options: Partial<BuyQuoteRequestOpts> = {},
): Promise<BuyQuote> {
const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = {
...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS,
@@ -164,7 +164,7 @@ export class AssetBuyer {
public async getBuyQuoteForERC20TokenAddressAsync(
tokenAddress: string,
assetBuyAmount: BigNumber,
- options: Partial<BuyQuoteRequestOpts>,
+ options: Partial<BuyQuoteRequestOpts> = {},
): Promise<BuyQuote> {
assert.isETHAddressHex('tokenAddress', tokenAddress);
assert.isBigNumber('assetBuyAmount', assetBuyAmount);
@@ -179,20 +179,23 @@ export class AssetBuyer {
*
* @return A promise of the txHash.
*/
- public async executeBuyQuoteAsync(buyQuote: BuyQuote, options: Partial<BuyQuoteExecutionOpts>): Promise<string> {
- const { rate, takerAddress, feeRecipient } = {
+ public async executeBuyQuoteAsync(
+ buyQuote: BuyQuote,
+ options: Partial<BuyQuoteExecutionOpts> = {},
+ ): Promise<string> {
+ 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)) {
@@ -207,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 b706ea143..cb0fd128c 100644
--- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts
+++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
@@ -1,11 +1,9 @@
-import { marketUtils } from '@0xproject/order-utils';
+import { marketUtils, rateUtils } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
-import { AssetBuyerError, BuyQuote, OrdersAndFillableAmounts } from '../types';
-
-import { orderUtils } from './order_utils';
+import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types';
// Calculates a buy quote for orders that have WETH as the takerAsset
export const buyQuoteCalculator = {
@@ -21,6 +19,7 @@ export const buyQuoteCalculator = {
const feeOrders = feeOrdersAndFillableAmounts.orders;
const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round();
+ // find the orders that cover the desired assetBuyAmount (with slippage)
const {
resultOrders,
remainingFillAmount,
@@ -29,9 +28,11 @@ export const buyQuoteCalculator = {
remainingFillableMakerAssetAmounts,
slippageBufferAmount,
});
+ // if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientAssetLiquidity);
}
+ // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage)
// TODO(bmillman): optimization
// update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
// finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
@@ -40,49 +41,139 @@ export const buyQuoteCalculator = {
remainingFeeAmount,
feeOrdersRemainingFillableMakerAssetAmounts,
} = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(resultOrders, feeOrders, {
- remainingFillableMakerAssetAmounts,
+ remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts,
});
+ // if we do not have enough feeOrders to cover the fees, throw
if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientZrxLiquidity);
}
+ // assetData information for the result
const assetData = orders[0].makerAssetData;
-
- // calculate minRate and maxRate by calculating min and max eth usage and then dividing into
- // assetBuyAmount to get assetData / WETH, needs to take into account feePercentage as well
- // minEthAmount = (sum(takerAssetAmount[i]) until sum(makerAssetAmount[i]) >= assetBuyAmount ) * (1 + feePercentage)
- // maxEthAmount = (sum(takerAssetAmount[i]) until i == orders.length) * (1 + feePercentage)
- const allOrders = _.concat(resultOrders, resultFeeOrders);
- const allRemainingAmounts = _.concat(
- ordersRemainingFillableMakerAssetAmounts,
- feeOrdersRemainingFillableMakerAssetAmounts,
+ // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount
+ const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
+ orders: resultOrders,
+ remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
+ };
+ const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
+ orders: resultFeeOrders,
+ remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts,
+ };
+ const bestCaseQuoteInfo = calculateQuoteInfo(
+ trimmedOrdersAndFillableAmounts,
+ trimmedFeeOrdersAndFillableAmounts,
+ assetBuyAmount,
+ feePercentage,
);
- let minEthAmount = constants.ZERO_AMOUNT;
- let maxEthAmount = constants.ZERO_AMOUNT;
- let cumulativeMakerAmount = constants.ZERO_AMOUNT;
- _.forEach(allOrders, (order, index) => {
- const remainingFillableMakerAssetAmount = allRemainingAmounts[index];
- const claimableTakerAssetAmount = orderUtils.calculateRemainingTakerAssetAmount(
- order,
- remainingFillableMakerAssetAmount,
- );
- // taker asset is always assumed to be WETH
- maxEthAmount = maxEthAmount.plus(claimableTakerAssetAmount);
- if (cumulativeMakerAmount.lessThan(assetBuyAmount)) {
- minEthAmount = minEthAmount.plus(claimableTakerAssetAmount);
- }
- cumulativeMakerAmount = cumulativeMakerAmount.plus(remainingFillableMakerAssetAmount);
- });
- const feeAdjustedMinRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount);
- const feeAdjustedMaxRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount);
+ // 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,
+ );
+
return {
assetData,
orders: resultOrders,
feeOrders: resultFeeOrders,
- minRate: feeAdjustedMinRate,
- maxRate: feeAdjustedMaxRate,
+ bestCaseQuoteInfo,
+ worstCaseQuoteInfo,
assetBuyAmount,
feePercentage,
};
},
};
+
+function calculateQuoteInfo(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+ feePercentage: number,
+): BuyQuoteInfo {
+ // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right
+ const [ethAmountToBuyAsset, zrxAmountToBuyAsset] = findEthAndZrxAmountNeededToBuyAsset(
+ ordersAndFillableAmounts,
+ assetBuyAmount,
+ );
+ // find the total eth needed to buy fees
+ const ethAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
+ const ethAmountBeforeAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees);
+ const totalEthAmount = ethAmountBeforeAffiliateFee.mul(feePercentage + 1);
+ // divide into the assetBuyAmount in order to find rate of makerAsset / WETH
+ const ethPerAssetPrice = ethAmountBeforeAffiliateFee.div(assetBuyAmount);
+ return {
+ totalEthAmount,
+ feeEthAmount: totalEthAmount.minus(ethAmountBeforeAffiliateFee),
+ ethPerAssetPrice,
+ };
+}
+
+// given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties
+function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFillableAmounts): OrdersAndFillableAmounts {
+ const ordersCopy = _.clone(ordersAndFillableAmounts.orders);
+ const remainingFillableMakerAssetAmountsCopy = _.clone(ordersAndFillableAmounts.remainingFillableMakerAssetAmounts);
+ return {
+ orders: ordersCopy.reverse(),
+ remainingFillableMakerAssetAmounts: remainingFillableMakerAssetAmountsCopy.reverse(),
+ };
+}
+
+function findEthAmountNeededToBuyFees(
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ feeAmount: BigNumber,
+): BigNumber {
+ const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts;
+ const result = _.reduce(
+ orders,
+ (acc, order, index) => {
+ const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
+ const amountToFill = BigNumber.min(acc.remainingFeeAmount, remainingFillableMakerAssetAmount);
+ const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfFeeOrder(order);
+ const ethAmountForThisOrder = feeAdjustedRate.mul(amountToFill);
+ return {
+ ethAmount: acc.ethAmount.plus(ethAmountForThisOrder),
+ remainingFeeAmount: BigNumber.max(constants.ZERO_AMOUNT, acc.remainingFeeAmount.minus(amountToFill)),
+ };
+ },
+ {
+ ethAmount: constants.ZERO_AMOUNT,
+ remainingFeeAmount: feeAmount,
+ },
+ );
+ return result.ethAmount;
+}
+
+function findEthAndZrxAmountNeededToBuyAsset(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+): [BigNumber, BigNumber] {
+ const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
+ const result = _.reduce(
+ orders,
+ (acc, order, index) => {
+ const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
+ const amountToFill = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount);
+ // find the amount of eth required to fill amountToFill (amountToFill / makerAssetAmount) * takerAssetAmount
+ const ethAmountForThisOrder = amountToFill
+ .mul(order.takerAssetAmount)
+ .dividedToIntegerBy(order.makerAssetAmount);
+ // find the amount of zrx required to fill fees for amountToFill (amountToFill / makerAssetAmount) * takerFee
+ const zrxAmountForThisOrder = amountToFill.mul(order.takerFee).dividedToIntegerBy(order.makerAssetAmount);
+ return {
+ ethAmount: acc.ethAmount.plus(ethAmountForThisOrder),
+ zrxAmount: acc.zrxAmount.plus(zrxAmountForThisOrder),
+ remainingAssetBuyAmount: BigNumber.max(
+ constants.ZERO_AMOUNT,
+ acc.remainingAssetBuyAmount.minus(amountToFill),
+ ),
+ };
+ },
+ {
+ ethAmount: constants.ZERO_AMOUNT,
+ zrxAmount: constants.ZERO_AMOUNT,
+ remainingAssetBuyAmount: assetBuyAmount,
+ },
+ );
+ return [result.ethAmount, result.zrxAmount];
+}
diff --git a/packages/asset-buyer/test/buy_quote_calculator_test.ts b/packages/asset-buyer/test/buy_quote_calculator_test.ts
new file mode 100644
index 000000000..b987b45a8
--- /dev/null
+++ b/packages/asset-buyer/test/buy_quote_calculator_test.ts
@@ -0,0 +1,160 @@
+import { orderFactory } from '@0xproject/order-utils/lib/src/order_factory';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import 'mocha';
+
+import { AssetBuyerError, OrdersAndFillableAmounts } from '../src/types';
+import { buyQuoteCalculator } from '../src/utils/buy_quote_calculator';
+
+import { chaiSetup } from './utils/chai_setup';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+// tslint:disable:custom-no-magic-numbers
+describe('buyQuoteCalculator', () => {
+ describe('#calculate', () => {
+ let ordersAndFillableAmounts: OrdersAndFillableAmounts;
+ let smallFeeOrderAndFillableAmount: OrdersAndFillableAmounts;
+ let allFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts;
+ beforeEach(() => {
+ // generate two orders for our desired maker asset
+ // the first order has a rate of 4 makerAsset / WETH with a takerFee of 200 ZRX and has only 200 / 400 makerAsset units left to fill (half fillable)
+ // the second order has a rate of 2 makerAsset / WETH with a takerFee of 100 ZRX and has 200 / 200 makerAsset units left to fill (completely fillable)
+ // generate one order for fees
+ // the fee order has a rate of 1 ZRX / WETH with no taker fee and has 100 ZRX left to fill (completely fillable)
+ const firstOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(400),
+ takerAssetAmount: new BigNumber(100),
+ takerFee: new BigNumber(200),
+ });
+ const firstRemainingFillAmount = new BigNumber(200);
+ const secondOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(200),
+ takerAssetAmount: new BigNumber(100),
+ takerFee: new BigNumber(100),
+ });
+ const secondRemainingFillAmount = secondOrder.makerAssetAmount;
+ ordersAndFillableAmounts = {
+ orders: [firstOrder, secondOrder],
+ remainingFillableMakerAssetAmounts: [firstRemainingFillAmount, secondRemainingFillAmount],
+ };
+ const smallFeeOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(100),
+ takerAssetAmount: new BigNumber(100),
+ });
+ smallFeeOrderAndFillableAmount = {
+ orders: [smallFeeOrder],
+ remainingFillableMakerAssetAmounts: [smallFeeOrder.makerAssetAmount],
+ };
+ const largeFeeOrder = orderFactory.createSignedOrderFromPartial({
+ makerAssetAmount: new BigNumber(110),
+ takerAssetAmount: new BigNumber(200),
+ takerFee: new BigNumber(10),
+ });
+ allFeeOrdersAndFillableAmounts = {
+ orders: [smallFeeOrder, largeFeeOrder],
+ remainingFillableMakerAssetAmounts: [
+ smallFeeOrder.makerAssetAmount,
+ largeFeeOrder.makerAssetAmount.minus(largeFeeOrder.takerFee),
+ ],
+ };
+ });
+ it('should throw if not enough maker asset liquidity', () => {
+ // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
+ expect(() =>
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(500),
+ 0,
+ 0,
+ ),
+ ).to.throw(AssetBuyerError.InsufficientAssetLiquidity);
+ });
+ it('should throw if not enough ZRX liquidity', () => {
+ // we request 300 makerAsset units but the ZRX order is only enough to fill the first order, which only has 200 makerAssetUnits available
+ expect(() =>
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ smallFeeOrderAndFillableAmount,
+ new BigNumber(300),
+ 0,
+ 0,
+ ),
+ ).to.throw(AssetBuyerError.InsufficientZrxLiquidity);
+ });
+ it('calculates a correct buyQuote with no slippage', () => {
+ // we request 200 makerAsset units which can be filled using the first order
+ // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
+ const assetBuyAmount = new BigNumber(200);
+ const feePercentage = 0.02;
+ const slippagePercentage = 0;
+ const buyQuote = buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ smallFeeOrderAndFillableAmount,
+ assetBuyAmount,
+ feePercentage,
+ slippagePercentage,
+ );
+ // test if orders are correct
+ expect(buyQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]);
+ 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 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.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);
+ });
+ it('calculates a correct buyQuote with with slippage', () => {
+ // we request 200 makerAsset units which can be filled using the first order
+ // however with 50% slippage we are protecting the buy with 100 extra makerAssetUnits
+ // so we need enough orders to fill 300 makerAssetUnits
+ // 300 makerAssetUnits can only be filled using both orders
+ // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
+ const assetBuyAmount = new BigNumber(200);
+ const feePercentage = 0.02;
+ const slippagePercentage = 0.5;
+ const buyQuote = buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ allFeeOrdersAndFillableAmounts,
+ assetBuyAmount,
+ feePercentage,
+ slippagePercentage,
+ );
+ // test if orders are correct
+ expect(buyQuote.orders).to.deep.equal(ordersAndFillableAmounts.orders);
+ 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 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 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);
+ });
+ });
+});
diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts
index 46a69ae4d..b1292903a 100644
--- a/packages/order-utils/src/order_factory.ts
+++ b/packages/order-utils/src/order_factory.ts
@@ -8,8 +8,21 @@ import { orderHashUtils } from './order_hash';
import { generatePseudoRandomSalt } from './salt';
import { signatureUtils } from './signature_utils';
import { CreateOrderOpts } from './types';
-
export const orderFactory = {
+ createOrderFromPartial(partialOrder: Partial<Order>): Order {
+ const defaultOrder = generateEmptyOrder();
+ return {
+ ...defaultOrder,
+ ...partialOrder,
+ };
+ },
+ createSignedOrderFromPartial(partialSignedOrder: Partial<SignedOrder>): SignedOrder {
+ const defaultOrder = generateEmptySignedOrder();
+ return {
+ ...defaultOrder,
+ ...partialSignedOrder,
+ };
+ },
createOrder(
makerAddress: string,
makerAssetAmount: BigNumber,
@@ -69,6 +82,30 @@ export const orderFactory = {
},
};
+function generateEmptySignedOrder(): SignedOrder {
+ return {
+ ...generateEmptyOrder(),
+ signature: constants.NULL_BYTES,
+ };
+}
+function generateEmptyOrder(): Order {
+ return {
+ senderAddress: constants.NULL_ADDRESS,
+ makerAddress: constants.NULL_ADDRESS,
+ takerAddress: constants.NULL_ADDRESS,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ makerAssetAmount: constants.ZERO_AMOUNT,
+ takerAssetAmount: constants.ZERO_AMOUNT,
+ makerAssetData: constants.NULL_BYTES,
+ takerAssetData: constants.NULL_BYTES,
+ salt: generatePseudoRandomSalt(),
+ exchangeAddress: constants.NULL_ADDRESS,
+ feeRecipientAddress: constants.NULL_ADDRESS,
+ expirationTimeSeconds: constants.INFINITE_TIMESTAMP_SEC,
+ };
+}
+
function generateDefaultCreateOrderOpts(): {
takerAddress: string;
senderAddress: string;