aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forwarder-helper/src/forwarder_helper_factory.ts
blob: e3ef59388769f2a9dfec91c526c5240cd97559aa (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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import { assert } from '@0xproject/assert';
import { APIOrder, HttpClient } from '@0xproject/connect';
import { ContractWrappers, OrderAndTraderInfo, OrderStatus } from '@0xproject/contract-wrappers';
import { schemas } from '@0xproject/json-schemas';
import { assetDataUtils } from '@0xproject/order-utils';
import { RemainingFillableCalculator } from '@0xproject/order-utils/lib/src/remaining_fillable_calculator';
import { RPCSubprovider, Web3ProviderEngine } from '@0xproject/subproviders';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';

import { constants } from './constants';
import { ForwarderHelperImpl, ForwarderHelperImplConfig } from './forwarder_helper_impl';
import { ForwarderHelper, ForwarderHelperFactoryError } from './types';
import { orderUtils } from './utils/order_utils';

export const forwarderHelperFactory = {
    /**
     * Given an array of orders and an array of feeOrders, get a ForwarderHelper
     * @param   orders      An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData
     * @param   feeOrders   An array of objects conforming to SignedOrder. Each order should specify ZRX as makerAssetData WETH as takerAssetData
     * @return  A ForwarderHelper, see type for definition
     */
    getForwarderHelperForOrders(orders: SignedOrder[], feeOrders: SignedOrder[] = []): ForwarderHelper {
        assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
        assert.doesConformToSchema('feeOrders', orders, schemas.signedOrdersSchema);
        const config: ForwarderHelperImplConfig = {
            orders,
            feeOrders,
        };
        const helper = new ForwarderHelperImpl(config);
        return helper;
    },
    /**
     * Given a desired makerAsset and SRA url, get a ForwarderHelper
     * @param   makerAssetData      An array of objects conforming to SignedOrder. Each order should specify the same makerAssetData and takerAssetData
     * @param   sraUrl              A url pointing to an SRA v2 compliant endpoint.
     * @param   rpcUrl              A url pointing to an ethereum node.
     * @param   networkId           The ethereum networkId, defaults to 1 (mainnet).
     * @return  A ForwarderHelper, see type for definition
     */
    async getForwarderHelperForMakerAssetDataAsync(
        makerAssetData: string,
        takerAddress: string,
        sraUrl: string,
        rpcUrl?: string,
        networkId: number = 1,
    ): Promise<ForwarderHelper> {
        assert.isHexString('makerAssetData', makerAssetData);
        assert.isETHAddressHex('takerAddress', takerAddress);
        assert.isWebUri('sraUrl', sraUrl);
        if (!_.isUndefined(rpcUrl)) {
            assert.isWebUri('rpcUrl', rpcUrl);
        }
        assert.isNumber('networkId', networkId);
        // create provider
        const providerEngine = new Web3ProviderEngine();
        if (!_.isUndefined(rpcUrl)) {
            providerEngine.addProvider(new RPCSubprovider(rpcUrl));
        }
        providerEngine.start();
        // create contract wrappers given provider and networkId
        const contractWrappers = new ContractWrappers(providerEngine, { networkId });
        // find ether token asset data
        const etherTokenAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists();
        if (_.isUndefined(etherTokenAddressIfExists)) {
            throw new Error(ForwarderHelperFactoryError.NoEtherTokenContractFound);
        }
        const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists);
        // find zrx token asset data
        let zrxTokenAssetData: string;
        try {
            zrxTokenAssetData = contractWrappers.exchange.getZRXAssetData();
        } catch (err) {
            throw new Error(ForwarderHelperFactoryError.NoZrxTokenContractFound);
        }
        // get orderbooks for makerAsset/WETH and ZRX/WETH
        const sraClient = new HttpClient(sraUrl);
        const orderbookRequests = [
            { baseAssetData: makerAssetData, quoteAssetData: etherTokenAssetData },
            { baseAssetData: zrxTokenAssetData, quoteAssetData: etherTokenAssetData },
        ];
        const requestOpts = { networkId };
        // TODO: try catch these requests and throw a more domain specific error
        const [makerAssetOrderbook, zrxOrderbook] = await Promise.all(
            _.map(orderbookRequests, request => sraClient.getOrderbookAsync(request, requestOpts)),
        );
        // validate orders and find remaining fillable from on chain state or sra api
        let ordersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts;
        let feeOrdersAndRemainingFillableMakerAssetAmounts: OrdersAndRemainingFillableMakerAssetAmounts;
        if (!_.isUndefined(rpcUrl)) {
            // if we do have an rpc url, get on-chain orders and traders info via the OrderValidatorWrapper
            const ordersFromSra = _.map(makerAssetOrderbook.asks.records, apiOrder => apiOrder.order);
            const feeOrdersFromSra = _.map(zrxOrderbook.asks.records, apiOrder => apiOrder.order);
            // TODO: try catch these requests and throw a more domain specific error
            const [makerAssetOrdersAndTradersInfo, feeOrdersAndTradersInfo] = await Promise.all(
                _.map([ordersFromSra, feeOrdersFromSra], ordersToBeValidated => {
                    const takerAddresses = _.map(ordersToBeValidated, () => takerAddress);
                    return contractWrappers.orderValidator.getOrdersAndTradersInfoAsync(
                        ordersToBeValidated,
                        takerAddresses,
                    );
                }),
            );
            // take maker asset orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts
            ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
                ordersFromSra,
                makerAssetOrdersAndTradersInfo,
                zrxTokenAssetData,
            );
            // take fee orders from SRA + on chain information and find the valid orders and remaining fillable maker asset amounts
            feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
                feeOrdersFromSra,
                feeOrdersAndTradersInfo,
                zrxTokenAssetData,
            );
        } else {
            // if we don't have an rpc url, assume all orders are valid and fallback to optional fill amounts from SRA
            // if fill amounts are not available from the SRA, assume all orders are completely fillable
            const apiOrdersFromSra = makerAssetOrderbook.asks.records;
            const feeApiOrdersFromSra = zrxOrderbook.asks.records;
            // take maker asset orders from SRA and the valid orders and remaining fillable maker asset amounts
            ordersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
                apiOrdersFromSra,
            );
            // take fee orders from SRA and find the valid orders and remaining fillable maker asset amounts
            feeOrdersAndRemainingFillableMakerAssetAmounts = getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
                feeApiOrdersFromSra,
            );
        }
        // compile final config
        const config: ForwarderHelperImplConfig = {
            orders: ordersAndRemainingFillableMakerAssetAmounts.orders,
            feeOrders: feeOrdersAndRemainingFillableMakerAssetAmounts.orders,
            remainingFillableMakerAssetAmounts:
                ordersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
            remainingFillableFeeAmounts:
                feeOrdersAndRemainingFillableMakerAssetAmounts.remainingFillableMakerAssetAmounts,
        };
        const helper = new ForwarderHelperImpl(config);
        return helper;
    },
};

interface OrdersAndRemainingFillableMakerAssetAmounts {
    orders: SignedOrder[];
    remainingFillableMakerAssetAmounts: BigNumber[];
}

/**
 * Given an array of APIOrder objects from a standard relayer api, return an array
 * of fillable orders with their corresponding remainingFillableMakerAssetAmounts
 */
function getValidOrdersAndRemainingFillableMakerAssetAmountsFromApi(
    apiOrders: APIOrder[],
): OrdersAndRemainingFillableMakerAssetAmounts {
    const result = _.reduce(
        apiOrders,
        (acc, apiOrder, index) => {
            // get current accumulations
            const { orders, remainingFillableMakerAssetAmounts } = acc;
            // get order and metadata
            const { order, metaData } = apiOrder;
            // if the order is expired, move on
            if (orderUtils.isOrderExpired(order)) {
                return acc;
            }
            // calculate remainingFillableMakerAssetAmount from api metadata
            const remainingFillableTakerAssetAmount = _.get(
                metaData,
                'remainingTakerAssetAmount',
                order.takerAssetAmount,
            );
            const remainingFillableMakerAssetAmount = orderUtils.calculateRemainingMakerAssetAmount(
                order,
                remainingFillableTakerAssetAmount,
            );
            // if there is some amount of maker asset left to fill and add the order and remaining amount to the accumulations
            // if there is not any maker asset left to fill, do not add
            if (remainingFillableMakerAssetAmount.gt(constants.ZERO_AMOUNT)) {
                return {
                    orders: _.concat(orders, order),
                    remainingFillableMakerAssetAmounts: _.concat(
                        remainingFillableMakerAssetAmounts,
                        remainingFillableMakerAssetAmount,
                    ),
                };
            } else {
                return acc;
            }
        },
        { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] },
    );
    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 getValidOrdersAndRemainingFillableMakerAssetAmountsFromOnChain(
    inputOrders: SignedOrder[],
    ordersAndTradersInfo: OrderAndTraderInfo[],
    zrxAssetData: string,
): OrdersAndRemainingFillableMakerAssetAmounts {
    // 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,
        (acc, order, index) => {
            // get current accumulations
            const { orders, remainingFillableMakerAssetAmounts } = acc;
            // get corresponding on-chain state for the order
            const { orderInfo, traderInfo } = ordersAndTradersInfo[index];
            // if the order IS NOT fillable, do not add anything and continue iterating
            if (orderInfo.orderStatus !== OrderStatus.FILLABLE) {
                return acc;
            }
            // 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.calculateRemainingMakerAssetAmount(
                order,
                remainingTakerAssetAmount,
            );
            const remainingFillableCalculator = new RemainingFillableCalculator(
                order.makerFee,
                order.makerAssetAmount,
                order.makerAssetData === zrxAssetData,
                transferrableAssetAmount,
                transferrableFeeAssetAmount,
                remainingMakerAssetAmount,
            );
            const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable();
            return {
                orders: _.concat(orders, order),
                remainingFillableMakerAssetAmounts: _.concat(
                    remainingFillableMakerAssetAmounts,
                    remainingFillableAmount,
                ),
            };
        },
        { orders: [] as SignedOrder[], remainingFillableMakerAssetAmounts: [] as BigNumber[] },
    );
    return result;
}