aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils/src/market_utils.ts
blob: 58e9ab7ba45376916dbdbb09cdb234afd717ff24 (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
import { schemas } from '@0xproject/json-schemas';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';

import { assert } from './assert';
import { constants } from './constants';
import { FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindOrdersThatCoverMakerAssetFillAmountOpts } 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   signedOrders                An array of objects that conform to the SignedOrder 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(
        signedOrders: SignedOrder[],
        makerAssetFillAmount: BigNumber,
        opts?: FindOrdersThatCoverMakerAssetFillAmountOpts,
    ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } {
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
        // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from signedOrders
        const remainingFillableMakerAssetAmounts = _.get(
            opts,
            'remainingFillableMakerAssetAmounts',
            _.map(signedOrders, order => order.makerAssetAmount),
        ) as BigNumber[];
        _.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
            assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
        );
        assert.assert(
            signedOrders.length === remainingFillableMakerAssetAmounts.length,
            'Expected signedOrders.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 signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount
        const result = _.reduce(
            signedOrders,
            ({ resultOrders, remainingFillAmount }, order, index) => {
                if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) {
                    return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT };
                } else {
                    const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
                    // 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: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT)
                            ? _.concat(resultOrders, order)
                            : resultOrders,
                        remainingFillAmount: BigNumber.max(
                            constants.ZERO_AMOUNT,
                            remainingFillAmount.minus(makerAssetAmountAvailable),
                        ),
                    };
                }
            },
            { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount },
        );
        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 signedOrders 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   signedOrders        An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
     *                              the makerAsset and WETH as the takerAsset.
     * @param   signedFeeOrders     An array of objects that conform to the SignedOrder 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(
        signedOrders: SignedOrder[],
        signedFeeOrders: SignedOrder[],
        opts?: FindFeeOrdersThatCoverFeesForTargetOrdersOpts,
    ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } {
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
        // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from signedOrders
        const remainingFillableMakerAssetAmounts = _.get(
            opts,
            'remainingFillableMakerAssetAmounts',
            _.map(signedOrders, order => order.makerAssetAmount),
        ) as BigNumber[];
        _.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
            assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
        );
        assert.assert(
            signedOrders.length === remainingFillableMakerAssetAmounts.length,
            'Expected signedOrders.length to equal opts.remainingFillableMakerAssetAmounts.length',
        );
        // try to get remainingFillableFeeAmounts from opts, if it's not there, use makerAssetAmount values from signedFeeOrders
        const remainingFillableFeeAmounts = _.get(
            opts,
            'remainingFillableFeeAmounts',
            _.map(signedFeeOrders, order => order.makerAssetAmount),
        ) as BigNumber[];
        _.forEach(remainingFillableFeeAmounts, (amount, index) =>
            assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount),
        );
        assert.assert(
            signedOrders.length === remainingFillableMakerAssetAmounts.length,
            'Expected signedFeeOrders.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 signedOrders
        const totalFeeAmount = _.reduce(
            signedOrders,
            (accFees, order, index) => {
                const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
                const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable
                    .mul(order.takerFee)
                    .div(order.makerAssetAmount);
                return accFees.plus(feeToFillMakerAssetAmountAvailable);
            },
            constants.ZERO_AMOUNT,
        );
        const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
            signedFeeOrders,
            totalFeeAmount,
            {
                remainingFillableMakerAssetAmounts: remainingFillableFeeAmounts,
                slippageBufferAmount,
            },
        );
        return {
            resultOrders,
            remainingFeeAmount: remainingFillAmount,
        };
        // 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
    },
};