163 lines
8.9 KiB
TypeScript
163 lines
8.9 KiB
TypeScript
import { schemas } from '@0xproject/json-schemas';
|
|
import { Order } from '@0xproject/types';
|
|
import { BigNumber } from '@0xproject/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
|
|
},
|
|
};
|