aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils/src/market_utils.ts
blob: fa32f14133581e67d17409e41534d1dba406c738 (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
import { schemas } from '@0x/json-schemas';
import { Order } from '@0x/types';
import { BigNumber } from '@0x/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
    },
};