aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils/src/market_utils.ts
blob: 94b5be4eb56a0b0fa0f7b032d8b60560819e660f (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
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';

export const marketUtils = {
    /**
     * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances,
     * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last.
     * 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   remainingFillableMakerAssetAmounts  An array of BigNumbers corresponding to the signedOrders parameter.
     *                                              You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
     *                                              for these values.
     * @param   makerAssetFillAmount                The amount of makerAsset desired to be filled.
     * @param   slippageBufferAmount                An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills.
     * @return  Resulting orders and remaining fill amount that could not be covered by the input.
     */
    findOrdersThatCoverMakerAssetFillAmount(
        signedOrders: SignedOrder[],
        remainingFillableMakerAssetAmounts: BigNumber[],
        makerAssetFillAmount: BigNumber,
        slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
    ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } {
        // type assertions
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        _.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
            assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
        );
        assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
        assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
        // other assertions
        assert.assert(
            signedOrders.length === remainingFillableMakerAssetAmounts.length,
            'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
        );
        // 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 (taking into account
     * on-chain balances, allowances, and partial fills) 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   remainingFillableMakerAssetAmounts  An array of BigNumbers corresponding to the signedOrders parameter.
     *                                              You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
     *                                              for these values.
     * @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   remainingFillableFeeAmounts         An array of BigNumbers corresponding to the signedFeeOrders parameter.
     *                                              You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
     *                                              for these values.
     * @param   slippageBufferAmount                An additional amount of fee to be covered by the result in case of trade collisions or partial fills.
     * @return  Resulting orders and remaining fee amount that could not be covered by the input.
     */
    findFeeOrdersThatCoverFeesForTargetOrders(
        signedOrders: SignedOrder[],
        remainingFillableMakerAssetAmounts: BigNumber[],
        signedFeeOrders: SignedOrder[],
        remainingFillableFeeAmounts: BigNumber[],
        slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
    ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } {
        // type assertions
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        _.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
            assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
        );
        assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
        _.forEach(remainingFillableFeeAmounts, (amount, index) =>
            assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount),
        );
        assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
        // other assertions
        assert.assert(
            signedOrders.length === remainingFillableMakerAssetAmounts.length,
            'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
        );
        assert.assert(
            signedOrders.length === remainingFillableMakerAssetAmounts.length,
            'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length',
        );
        // 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,
            remainingFillableFeeAmounts,
            totalFeeAmount,
            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
    },
};