import { marketUtils, rateUtils, SignedOrder } from '@0x/order-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { constants } from '../constants'; import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types'; import { orderUtils } from './order_utils'; // Calculates a buy quote for orders that have WETH as the takerAsset export const buyQuoteCalculator = { calculate( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, assetBuyAmount: BigNumber, feePercentage: number, slippagePercentage: number, isMakerAssetZrxToken: boolean, ): BuyQuote { const orders = ordersAndFillableAmounts.orders; const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts; 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, ordersRemainingFillableMakerAssetAmounts, } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetBuyAmount, { 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); } // if we are not buying ZRX: // 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 let resultFeeOrders = [] as SignedOrder[]; let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[]; if (!isMakerAssetZrxToken) { const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( resultOrders, feeOrders, { remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts, remainingFillableFeeAmounts, }, ); // if we do not have enough feeOrders to cover the fees, throw if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) { throw new Error(AssetBuyerError.InsufficientZrxLiquidity); } resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders; feeOrdersRemainingFillableMakerAssetAmounts = feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts; } // assetData information for the result const assetData = orders[0].makerAssetData; // 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, 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, isMakerAssetZrxToken, ); return { assetData, orders: resultOrders, feeOrders: resultFeeOrders, bestCaseQuoteInfo, worstCaseQuoteInfo, assetBuyAmount, feePercentage, }; }, }; function calculateQuoteInfo( ordersAndFillableAmounts: OrdersAndFillableAmounts, feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, assetBuyAmount: BigNumber, feePercentage: number, isMakerAssetZrxToken: boolean, ): BuyQuoteInfo { // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right let ethAmountToBuyAsset = constants.ZERO_AMOUNT; let ethAmountToBuyZrx = constants.ZERO_AMOUNT; if (isMakerAssetZrxToken) { ethAmountToBuyAsset = findEthAmountNeededToBuyZrx(ordersAndFillableAmounts, assetBuyAmount); } else { // find eth and zrx amounts needed to buy const ethAndZrxAmountToBuyAsset = findEthAndZrxAmountNeededToBuyAsset(ordersAndFillableAmounts, assetBuyAmount); ethAmountToBuyAsset = ethAndZrxAmountToBuyAsset[0]; const zrxAmountToBuyAsset = ethAndZrxAmountToBuyAsset[1]; // find eth amount needed to buy zrx ethAmountToBuyZrx = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); } /// find the eth amount needed to buy the affiliate fee const ethAmountToBuyAffiliateFee = ethAmountToBuyAsset.mul(feePercentage); const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyZrx); const ethAmountTotal = totalEthAmountWithoutAffiliateFee.plus(ethAmountToBuyAffiliateFee); // divide into the assetBuyAmount in order to find rate of makerAsset / WETH const ethPerAssetPrice = totalEthAmountWithoutAffiliateFee.div(assetBuyAmount); return { totalEthAmount: ethAmountTotal, feeEthAmount: ethAmountToBuyAffiliateFee, 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 findEthAmountNeededToBuyZrx( feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, zrxBuyAmount: BigNumber, ): BigNumber { const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts; const result = _.reduce( orders, (acc, order, index) => { const { totalEthAmount, remainingZrxBuyAmount } = acc; const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; const makerFillAmount = BigNumber.min(acc.remainingZrxBuyAmount, remainingFillableMakerAssetAmount); const [takerFillAmount, adjustedMakerFillAmount] = orderUtils.getTakerFillAmountForFeeOrder( order, makerFillAmount, ); const extraFeeAmount = remainingFillableMakerAssetAmount.greaterThanOrEqualTo(adjustedMakerFillAmount) ? constants.ZERO_AMOUNT : adjustedMakerFillAmount.sub(makerFillAmount); return { totalEthAmount: totalEthAmount.plus(takerFillAmount), remainingZrxBuyAmount: BigNumber.max( constants.ZERO_AMOUNT, acc.remainingZrxBuyAmount.minus(makerFillAmount).plus(extraFeeAmount), ), }; }, { totalEthAmount: constants.ZERO_AMOUNT, remainingZrxBuyAmount: zrxBuyAmount, }, ); return result.totalEthAmount; } function findEthAndZrxAmountNeededToBuyAsset( ordersAndFillableAmounts: OrdersAndFillableAmounts, assetBuyAmount: BigNumber, ): [BigNumber, BigNumber] { const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts; const result = _.reduce( orders, (acc, order, index) => { const { totalEthAmount, totalZrxAmount, remainingAssetBuyAmount } = acc; const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; const makerFillAmount = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount); const takerFillAmount = orderUtils.getTakerFillAmount(order, makerFillAmount); const takerFeeAmount = orderUtils.getTakerFeeAmount(order, takerFillAmount); return { totalEthAmount: totalEthAmount.plus(takerFillAmount), totalZrxAmount: totalZrxAmount.plus(takerFeeAmount), remainingAssetBuyAmount: BigNumber.max( constants.ZERO_AMOUNT, remainingAssetBuyAmount.minus(makerFillAmount), ), }; }, { totalEthAmount: constants.ZERO_AMOUNT, totalZrxAmount: constants.ZERO_AMOUNT, remainingAssetBuyAmount: assetBuyAmount, }, ); return [result.totalEthAmount, result.totalZrxAmount]; }