aboutsummaryrefslogtreecommitdiffstats
path: root/packages/contracts/test/utils/forwarder_wrapper.ts
blob: e39df14b181de81053aa064a05d011372523d316 (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
import { assetDataUtils } from '@0xproject/order-utils';
import { AssetProxyId, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import { Provider, TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types';
import * as _ from 'lodash';

import { ForwarderContract } from '../../generated_contract_wrappers/forwarder';

import { constants } from './constants';
import { formatters } from './formatters';
import { LogDecoder } from './log_decoder';
import { MarketSellOrders } from './types';

const DEFAULT_FEE_PROPORTION = 0;
const PERCENTAGE_DENOMINATOR = 10000;
const ZERO_AMOUNT = new BigNumber(0);
const INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT = 'Unable to satisfy makerAssetFillAmount with provided orders';

export class ForwarderWrapper {
    private readonly _web3Wrapper: Web3Wrapper;
    private readonly _forwarderContract: ForwarderContract;
    private readonly _logDecoder: LogDecoder;
    private readonly _zrxAddress: string;
    private static _createOptimizedSellOrders(signedOrders: SignedOrder[]): MarketSellOrders {
        const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT);
        const assetDataId = assetDataUtils.decodeAssetProxyId(signedOrders[0].makerAssetData);
        // Contract will fill this in for us as all of the assetData is assumed to be the same
        for (let i = 0; i < signedOrders.length; i++) {
            if (i !== 0 && assetDataId === AssetProxyId.ERC20) {
                // Forwarding contract will fill this in from the first order
                marketSellOrders.orders[i].makerAssetData = constants.NULL_BYTES;
            }
            marketSellOrders.orders[i].takerAssetData = constants.NULL_BYTES;
        }
        return marketSellOrders;
    }
    private static _createOptimizedZRXSellOrders(signedOrders: SignedOrder[]): MarketSellOrders {
        const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT);
        // Contract will fill this in for us as all of the assetData is assumed to be the same
        for (let i = 0; i < signedOrders.length; i++) {
            marketSellOrders.orders[i].makerAssetData = constants.NULL_BYTES;
            marketSellOrders.orders[i].takerAssetData = constants.NULL_BYTES;
        }
        return marketSellOrders;
    }
    private static _calculateAdditionalFeeProportionAmount(feeProportion: number, fillAmountWei: BigNumber): BigNumber {
        if (feeProportion > 0) {
            // Add to the total ETH transaction to ensure all NFTs can be filled after fees
            // 150 = 1.5% = 0.015
            const denominator = new BigNumber(1).minus(new BigNumber(feeProportion).dividedBy(PERCENTAGE_DENOMINATOR));
            return fillAmountWei.dividedBy(denominator).round(0, BigNumber.ROUND_FLOOR);
        }
        return fillAmountWei;
    }
    constructor(contractInstance: ForwarderContract, provider: Provider, zrxAddress: string) {
        this._forwarderContract = contractInstance;
        this._web3Wrapper = new Web3Wrapper(provider);
        this._logDecoder = new LogDecoder(this._web3Wrapper, this._forwarderContract.address);
        // this._web3Wrapper.abiDecoder.addABI(contractInstance.abi);
        this._zrxAddress = zrxAddress;
    }
    public async marketBuyTokensWithEthAsync(
        orders: SignedOrder[],
        feeOrders: SignedOrder[],
        makerTokenBuyAmount: BigNumber,
        txData: TxDataPayable,
        opts: { feeProportion?: number; feeRecipient?: string } = {},
    ): Promise<TransactionReceiptWithDecodedLogs> {
        const params = ForwarderWrapper._createOptimizedSellOrders(orders);
        const feeParams = ForwarderWrapper._createOptimizedZRXSellOrders(feeOrders);
        const feeProportion = _.isUndefined(opts.feeProportion) ? DEFAULT_FEE_PROPORTION : opts.feeProportion;
        const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient;
        const txHash: string = await this._forwarderContract.marketBuyTokensWithEth.sendTransactionAsync(
            params.orders,
            params.signatures,
            feeParams.orders,
            feeParams.signatures,
            makerTokenBuyAmount,
            feeProportion,
            feeRecipient,
            txData,
        );
        const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
        return tx;
    }
    public async marketSellEthForERC20Async(
        orders: SignedOrder[],
        feeOrders: SignedOrder[],
        txData: TxDataPayable,
        opts: { feeProportion?: number; feeRecipient?: string } = {},
    ): Promise<TransactionReceiptWithDecodedLogs> {
        const assetDataId = assetDataUtils.decodeAssetProxyId(orders[0].makerAssetData);
        if (assetDataId !== AssetProxyId.ERC20) {
            throw new Error('Asset type not supported by marketSellEthForERC20');
        }
        const params = ForwarderWrapper._createOptimizedSellOrders(orders);
        const feeParams = ForwarderWrapper._createOptimizedZRXSellOrders(feeOrders);
        const feeProportion = _.isUndefined(opts.feeProportion) ? DEFAULT_FEE_PROPORTION : opts.feeProportion;
        const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient;
        const txHash: string = await this._forwarderContract.marketSellEthForERC20.sendTransactionAsync(
            params.orders,
            params.signatures,
            feeParams.orders,
            feeParams.signatures,
            feeProportion,
            feeRecipient,
            txData,
        );
        const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
        return tx;
    }
    public async calculateMarketBuyFillAmountWeiAsync(
        orders: SignedOrder[],
        feeOrders: SignedOrder[],
        feeProportion: number,
        makerAssetFillAmount: BigNumber,
    ): Promise<BigNumber> {
        const assetProxyId = assetDataUtils.decodeAssetProxyId(orders[0].makerAssetData);
        switch (assetProxyId) {
            case AssetProxyId.ERC20: {
                const fillAmountWei = this._calculateMarketBuyERC20FillAmountAsync(
                    orders,
                    feeOrders,
                    feeProportion,
                    makerAssetFillAmount,
                );
                return fillAmountWei;
            }
            case AssetProxyId.ERC721: {
                const fillAmountWei = await this._calculateMarketBuyERC721FillAmountAsync(
                    orders,
                    feeOrders,
                    feeProportion,
                );
                return fillAmountWei;
            }
            default:
                throw new Error(`Invalid Asset Proxy Id: ${assetProxyId}`);
        }
    }
    private async _calculateMarketBuyERC20FillAmountAsync(
        orders: SignedOrder[],
        feeOrders: SignedOrder[],
        feeProportion: number,
        makerAssetFillAmount: BigNumber,
    ): Promise<BigNumber> {
        const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData);
        const makerAssetToken = makerAssetData.tokenAddress;
        const params = formatters.createMarketBuyOrders(orders, makerAssetFillAmount);

        let fillAmountWei;
        if (makerAssetToken === this._zrxAddress) {
            // If buying ZRX we buy the tokens and fees from the ZRX order in one step
            const expectedBuyFeeTokensFillResults = await this._forwarderContract.calculateMarketBuyZrxResults.callAsync(
                params.orders,
                makerAssetFillAmount,
            );
            if (expectedBuyFeeTokensFillResults.makerAssetFilledAmount.lessThan(makerAssetFillAmount)) {
                throw new Error(INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT);
            }
            fillAmountWei = expectedBuyFeeTokensFillResults.takerAssetFilledAmount;
        } else {
            const expectedMarketBuyFillResults = await this._forwarderContract.calculateMarketBuyResults.callAsync(
                params.orders,
                makerAssetFillAmount,
            );
            if (expectedMarketBuyFillResults.makerAssetFilledAmount.lessThan(makerAssetFillAmount)) {
                throw new Error(INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT);
            }
            fillAmountWei = expectedMarketBuyFillResults.takerAssetFilledAmount;
            const expectedFeeAmount = expectedMarketBuyFillResults.takerFeePaid;
            if (expectedFeeAmount.greaterThan(ZERO_AMOUNT)) {
                const expectedFeeFillFillAmountWei = await this._calculateMarketBuyERC20FillAmountAsync(
                    feeOrders,
                    [],
                    DEFAULT_FEE_PROPORTION,
                    expectedFeeAmount,
                );
                fillAmountWei = fillAmountWei.plus(expectedFeeFillFillAmountWei);
            }
        }
        fillAmountWei = ForwarderWrapper._calculateAdditionalFeeProportionAmount(feeProportion, fillAmountWei);
        return fillAmountWei;
    }
    private async _calculateMarketBuyERC721FillAmountAsync(
        orders: SignedOrder[],
        feeOrders: SignedOrder[],
        feeProportion: number,
    ): Promise<BigNumber> {
        // Total cost when buying ERC721 is the total cost of all ERC721 orders + any fee abstraction
        let fillAmountWei = _.reduce(
            orders,
            (totalAmount: BigNumber, order: SignedOrder) => {
                return totalAmount.plus(order.takerAssetAmount);
            },
            ZERO_AMOUNT,
        );
        const totalFees = _.reduce(
            orders,
            (totalAmount: BigNumber, order: SignedOrder) => {
                return totalAmount.plus(order.takerFee);
            },
            ZERO_AMOUNT,
        );
        if (totalFees.greaterThan(ZERO_AMOUNT)) {
            // Calculate the ZRX fee abstraction cost
            const emptyFeeOrders: SignedOrder[] = [];
            const expectedFeeAmountWei = await this._calculateMarketBuyERC20FillAmountAsync(
                feeOrders,
                emptyFeeOrders,
                DEFAULT_FEE_PROPORTION,
                totalFees,
            );
            fillAmountWei = fillAmountWei.plus(expectedFeeAmountWei);
        }
        fillAmountWei = ForwarderWrapper._calculateAdditionalFeeProportionAmount(feeProportion, fillAmountWei);
        return fillAmountWei;
    }
}