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 { // 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; }