import { schemas } from '@0x/json-schemas';
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { constants } from './constants';
import {
FeeOrdersAndRemainingFeeAmount,
FindFeeOrdersThatCoverFeesForTargetOrdersOpts,
FindOrdersThatCoverMakerAssetFillAmountOpts,
OrdersAndRemainingFillAmount,
} from './types';
export const marketUtils = {
/**
* Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount
* in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last order.
* Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH.
* @param orders An array of objects that extend the Order interface. All orders should specify the same makerAsset.
* All orders should specify WETH as the takerAsset.
* @param makerAssetFillAmount The amount of makerAsset desired to be filled.
* @param opts Optional arguments this function accepts.
* @return Resulting orders and remaining fill amount that could not be covered by the input.
*/
findOrdersThatCoverMakerAssetFillAmount<T extends Order>(
orders: T[],
makerAssetFillAmount: BigNumber,
opts?: FindOrdersThatCoverMakerAssetFillAmountOpts,
): OrdersAndRemainingFillAmount<T> {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
// try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders
const remainingFillableMakerAssetAmounts = _.get(
opts,
'remainingFillableMakerAssetAmounts',
_.map(orders, order => order.makerAssetAmount),
) as BigNumber[];
_.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
);
assert.assert(
orders.length === remainingFillableMakerAssetAmounts.length,
'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length',
);
// try to get slippageBufferAmount from opts, if it's not there, default to 0
const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber;
assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount);
// calculate total amount of makerAsset needed to be filled
const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount);
// iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount
const result = _.reduce(
orders,
({ resultOrders, remainingFillAmount, ordersRemainingFillableMakerAssetAmounts }, order, index) => {
if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) {
return {
resultOrders,
remainingFillAmount: constants.ZERO_AMOUNT,
ordersRemainingFillableMakerAssetAmounts,
};
} else {
const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
const shouldIncludeOrder = makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT);
// if there is no makerAssetAmountAvailable do not append order to resultOrders
// if we have exceeded the total amount we want to fill set remainingFillAmount to 0
return {
resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders,
ordersRemainingFillableMakerAssetAmounts: shouldIncludeOrder
? _.concat(ordersRemainingFillableMakerAssetAmounts, makerAssetAmountAvailable)
: ordersRemainingFillableMakerAssetAmounts,
remainingFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingFillAmount.minus(makerAssetAmountAvailable),
),
};
}
},
{
resultOrders: [] as T[],
remainingFillAmount: totalFillAmount,
ordersRemainingFillableMakerAssetAmounts: [] as BigNumber[],
},
);
return result;
},
/**
* Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX
* in order to fill the takerFees required by orders plus a slippageBufferAmount.
* Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of
* feeOrders that will cost the least ETH.
* @param orders An array of objects that extend the Order interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param feeOrders An array of objects that extend the Order interface. All orders should specify ZRX as
* the makerAsset and WETH as the takerAsset.
* @param opts Optional arguments this function accepts.
* @return Resulting orders and remaining fee amount that could not be covered by the input.
*/
findFeeOrdersThatCoverFeesForTargetOrders<T extends Order>(
orders: T[],
feeOrders: T[],
opts?: FindFeeOrdersThatCoverFeesForTargetOrdersOpts,
): FeeOrdersAndRemainingFeeAmount<T> {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema);
// try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders
const remainingFillableMakerAssetAmounts = _.get(
opts,
'remainingFillableMakerAssetAmounts',
_.map(orders, order => order.makerAssetAmount),
) as BigNumber[];
_.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
);
assert.assert(
orders.length === remainingFillableMakerAssetAmounts.length,
'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length',
);
// try to get remainingFillableFeeAmounts from opts, if it's not there, use makerAssetAmount values from feeOrders
const remainingFillableFeeAmounts = _.get(
opts,
'remainingFillableFeeAmounts',
_.map(feeOrders, order => order.makerAssetAmount),
) as BigNumber[];
_.forEach(remainingFillableFeeAmounts, (amount, index) =>
assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount),
);
assert.assert(
feeOrders.length === remainingFillableFeeAmounts.length,
'Expected feeOrders.length to equal opts.remainingFillableFeeAmounts.length',
);
// try to get slippageBufferAmount from opts, if it's not there, default to 0
const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber;
assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount);
// calculate total amount of ZRX needed to fill orders
const totalFeeAmount = _.reduce(
orders,
(accFees, order, index) => {
const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable
.mul(order.takerFee)
.dividedToIntegerBy(order.makerAssetAmount);
return accFees.plus(feeToFillMakerAssetAmountAvailable);
},
constants.ZERO_AMOUNT,
);
const {
resultOrders,
remainingFillAmount,
ordersRemainingFillableMakerAssetAmounts,
} = marketUtils.findOrdersThatCoverMakerAssetFillAmount(feeOrders, totalFeeAmount, {
remainingFillableMakerAssetAmounts: remainingFillableFeeAmounts,
slippageBufferAmount,
});
return {
resultFeeOrders: resultOrders,
remainingFeeAmount: remainingFillAmount,
feeOrdersRemainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
};
// TODO: add more orders here to cover rounding
// https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx
},
};