import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0x/contract-wrappers';
import { sortingUtils } from '@0x/order-utils';
import { RemainingFillableCalculator } from '@0x/order-utils/lib/src/remaining_fillable_calculator';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
AssetBuyerError,
OrderProviderRequest,
OrderProviderResponse,
OrdersAndFillableAmounts,
SignedOrderWithRemainingFillableMakerAssetAmount,
} from '../types';
import { orderUtils } from './order_utils';
export const orderProviderResponseProcessor = {
throwIfInvalidResponse(response: OrderProviderResponse, request: OrderProviderRequest): void {
const { makerAssetData, takerAssetData } = request;
_.forEach(response.orders, order => {
if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) {
throw new Error(AssetBuyerError.InvalidOrderProviderResponse);
}
});
},
/**
* 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(
orderProviderResponse: OrderProviderResponse,
isMakerAssetZrxToken: boolean,
expiryBufferSeconds: number,
orderValidator?: OrderValidatorWrapper,
): Promise<OrdersAndFillableAmounts> {
// drop orders that are expired or not open
const filteredOrders = filterOutExpiredAndNonOpenOrders(orderProviderResponse.orders, expiryBufferSeconds);
// set the orders to be sorted equal to the filtered orders
let unsortedOrders = filteredOrders;
// if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts
if (!_.isUndefined(orderValidator)) {
const takerAddresses = _.map(filteredOrders, () => constants.NULL_ADDRESS);
try {
const ordersAndTradersInfo = await orderValidator.getOrdersAndTradersInfoAsync(
filteredOrders,
takerAddresses,
);
// take orders + on chain information and find the valid orders and remaining fillable maker asset amounts
unsortedOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
filteredOrders,
ordersAndTradersInfo,
isMakerAssetZrxToken,
);
} catch (err) {
// Sometimes we observe this call to orderValidator fail with response `0x`
// Because of differences in Parity / Geth implementations, its very hard to tell if this response is a "system error"
// or a revert. In this case we just swallow these errors and fallback to partial fill information from the SRA.
// TODO(bmillman): report these errors so we have an idea of how often we're getting these failures.
}
}
// 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 sortedOrders = isMakerAssetZrxToken
? sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedOrders)
: sortingUtils.sortOrdersByFeeAdjustedRate(unsortedOrders);
// unbundle orders and fillable amounts and compile final result
const result = unbundleOrdersWithAmounts(sortedOrders);
return result;
},
};
/**
* Given an array of orders, return a new array with expired and non open orders filtered out.
*/
function filterOutExpiredAndNonOpenOrders(
orders: SignedOrderWithRemainingFillableMakerAssetAmount[],
expiryBufferSeconds: number,
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
const result = _.filter(orders, order => {
return orderUtils.isOpenOrder(order) && !orderUtils.willOrderExpire(order, expiryBufferSeconds);
});
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[],
isMakerAssetZrxToken: boolean,
): 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.getRemainingMakerAmount(order, remainingTakerAssetAmount);
const remainingFillableCalculator = new RemainingFillableCalculator(
order.makerFee,
order.makerAssetAmount,
isMakerAssetZrxToken,
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[],
): OrdersAndFillableAmounts {
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;
}