221 lines
10 KiB
TypeScript
221 lines
10 KiB
TypeScript
import { assetProxyUtils } 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 _web3Wrapper: Web3Wrapper;
|
|
private _forwarderContract: ForwarderContract;
|
|
private _logDecoder: LogDecoder;
|
|
private _zrxAddress: string;
|
|
private static _createOptimizedSellOrders(signedOrders: SignedOrder[]): MarketSellOrders {
|
|
const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT);
|
|
const assetDataId = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.decodeAssetData(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;
|
|
}
|
|
}
|