aboutsummaryrefslogtreecommitdiffstats
path: root/packages/asset-buyer/src/utils/order_provider_response_processor.ts
blob: 4244d196cdee91a237daf87eb9cda6518aab64b1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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;
}