import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0xproject/contract-wrappers';
import { sortingUtils } from '@0xproject/order-utils';
import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
AssetBuyerOrdersAndFillableAmounts,
OrderProviderResponse,
SignedOrderWithRemainingFillableMakerAssetAmount,
} from '../types';
import { orderUtils } from './order_utils';
interface OrdersAndRemainingFillableMakerAssetAmounts {
orders: SignedOrder[];
remainingFillableMakerAssetAmounts: BigNumber[];
}
export const orderProviderResponseProcessor = {
/**
* Take the responses for the target orders to buy and fee orders and process them.
* Processing includes:
* - Drop orders that are expired or not open orders (null taker address)
* - If shouldValidateOnChain, attempt to grab fillable amounts from on-chain otherwise assume completely fillable
* - Sort by rate
*/
async processAsync(
targetOrderProviderResponse: OrderProviderResponse,
feeOrderProviderResponse: OrderProviderResponse,
zrxTokenAssetData: string,
orderValidator?: OrderValidatorWrapper,
): Promise<AssetBuyerOrdersAndFillableAmounts> {
// drop orders that are expired or not open
const filteredTargetOrders = filterOutExpiredAndNonOpenOrders(targetOrderProviderResponse.orders);
const filteredFeeOrders = filterOutExpiredAndNonOpenOrders(feeOrderProviderResponse.orders);
// set the orders to be sorted equal to the filtered orders
let unsortedTargetOrders = filteredTargetOrders;
let unsortedFeeOrders = filteredFeeOrders;
// if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts
if (!_.isUndefined(orderValidator)) {
// TODO(bmillman): improvement
// try catch these requests and throw a more domain specific error
// TODO(bmillman): optimization
// reduce this to once RPC call buy combining orders into one array and then splitting up the response
const [targetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all(
_.map([filteredTargetOrders, filteredFeeOrders], ordersToBeValidated => {
const takerAddresses = _.map(ordersToBeValidated, () => constants.NULL_ADDRESS);
return orderValidator.getOrdersAndTradersInfoAsync(ordersToBeValidated, takerAddresses);
}),
);
// take orders + on chain information and find the valid orders and remaining fillable maker asset amounts
unsortedTargetOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
filteredTargetOrders,
targetOrdersAndTradersInfo,
zrxTokenAssetData,
);
// take orders + on chain information and find the valid orders and remaining fillable maker asset amounts
unsortedFeeOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
filteredFeeOrders,
feeOrdersAndTradersInfo,
zrxTokenAssetData,
);
}
// sort orders by rate
// TODO(bmillman): optimization
// provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens
const sortedTargetOrders = sortingUtils.sortOrdersByFeeAdjustedRate(unsortedTargetOrders);
const sortedFeeOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedFeeOrders);
// unbundle orders and fillable amounts and compile final result
const targetOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedTargetOrders);
const feeOrdersAndRemainingFillableMakerAssetAmounts = unbundleOrdersWithAmounts(sortedFeeOrders);
return {
orders: targetOrdersAndRemainingFillableMakerAssetAmounts.orders,
feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders,
remainingFillableMakerAssetAmounts:
targetOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts:
feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
};
},
};
/**
* Given an array of orders, return a new array with expired and non open orders filtered out.
*/
function filterOutExpiredAndNonOpenOrders(
orders: SignedOrderWithRemainingFillableMakerAssetAmount[],
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
const result = _.filter(orders, order => {
return orderUtils.isOpenOrder(order) && !orderUtils.isOrderExpired(order);
});
return result;
}
/**
* Given an array of orders and corresponding on-chain infos, return a subset of the orders
* that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts.
*/
function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
inputOrders: SignedOrder[],
ordersAndTradersInfo: OrderAndTraderInfo[],
zrxAssetData: string,
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
// iterate through the input orders and find the ones that are still fillable
// for the orders that are still fillable, calculate the remaining fillable maker asset amount
const result = _.reduce(
inputOrders,
(accOrders, order, index) => {
// get corresponding on-chain state for the order
const { orderInfo, traderInfo } = ordersAndTradersInfo[index];
// if the order IS NOT fillable, do not add anything to the accumulations and continue iterating
if (orderInfo.orderStatus !== OrderStatus.FILLABLE) {
return accOrders;
}
// if the order IS fillable, add the order and calculate the remaining fillable amount
const transferrableAssetAmount = BigNumber.min([traderInfo.makerAllowance, traderInfo.makerBalance]);
const transferrableFeeAssetAmount = BigNumber.min([
traderInfo.makerZrxAllowance,
traderInfo.makerZrxBalance,
]);
const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount);
const remainingMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount(
order,
remainingTakerAssetAmount,
);
const remainingFillableCalculator = new RemainingFillableCalculator(
order.makerFee,
order.makerAssetAmount,
order.makerAssetData === zrxAssetData,
transferrableAssetAmount,
transferrableFeeAssetAmount,
remainingMakerAssetAmount,
);
const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable();
// if the order does not have any remaining fillable makerAsset, do not add anything to the accumulations and continue iterating
if (remainingFillableAmount.lte(constants.ZERO_AMOUNT)) {
return accOrders;
}
const orderWithRemainingFillableMakerAssetAmount = {
...order,
remainingFillableMakerAssetAmount: remainingFillableAmount,
};
const newAccOrders = _.concat(accOrders, orderWithRemainingFillableMakerAssetAmount);
return newAccOrders;
},
[] as SignedOrderWithRemainingFillableMakerAssetAmount[],
);
return result;
}
/**
* Given an array of orders with remaining fillable maker asset amounts. Unbundle into an instance of OrdersAndRemainingFillableMakerAssetAmounts.
* If an order is missing a corresponding remainingFillableMakerAssetAmount, assume it is completely fillable.
*/
function unbundleOrdersWithAmounts(
ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[],
): OrdersAndRemainingFillableMakerAssetAmounts {
const result = _.reduce(
ordersWithAmounts,
(acc, orderWithAmount) => {
const { orders, remainingFillableMakerAssetAmounts } = acc;
const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount;
// if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable
const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount;
// if remaining amount is less than or equal to zero, do not add it
if (newRemainingAmount.lte(constants.ZERO_AMOUNT)) {
return acc;
}
const newAcc = {
orders: _.concat(orders, order),
remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount),
};
return newAcc;
},
{
orders: [] as SignedOrder[],
remainingFillableMakerAssetAmounts: [] as BigNumber[],
},
);
return result;
}