From 0557d6a9bfc07b8d360970ffbcf582f8a26943cb Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Fri, 6 Jul 2018 15:00:09 +1000 Subject: Forwarding contract (squashed commits) --- packages/contracts/test/utils/forwarder_wrapper.ts | 220 +++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 packages/contracts/test/utils/forwarder_wrapper.ts (limited to 'packages/contracts/test/utils/forwarder_wrapper.ts') diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts new file mode 100644 index 000000000..d227420ee --- /dev/null +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -0,0 +1,220 @@ +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 { + 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 { + 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 { + 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 { + 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 { + // 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; + } +} -- cgit v1.2.3