import { DummyERC20TokenContract, DummyERC721TokenContract, ExchangeContract } from '@0x/abi-gen-wrappers'; import * as artifacts from '@0x/contract-artifacts'; import { assetDataUtils } from '@0x/order-utils'; import { orderFactory } from '@0x/order-utils/lib/src/order_factory'; import { OrderWithoutExchangeAddress, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; import { constants } from './constants'; export class FillScenarios { private readonly _web3Wrapper: Web3Wrapper; private readonly _userAddresses: string[]; private readonly _coinbase: string; private readonly _zrxTokenAddress: string; private readonly _exchangeAddress: string; private readonly _erc20ProxyAddress: string; private readonly _erc721ProxyAddress: string; constructor( provider: Provider, userAddresses: string[], zrxTokenAddress: string, exchangeAddress: string, erc20ProxyAddress: string, erc721ProxyAddress: string, ) { this._web3Wrapper = new Web3Wrapper(provider); this._userAddresses = userAddresses; this._coinbase = userAddresses[0]; this._zrxTokenAddress = zrxTokenAddress; this._exchangeAddress = exchangeAddress; this._erc20ProxyAddress = erc20ProxyAddress; this._erc721ProxyAddress = erc721ProxyAddress; } public async createFillableSignedOrderAsync( makerAssetData: string, takerAssetData: string, makerAddress: string, takerAddress: string, fillableAmount: BigNumber, expirationTimeSeconds?: BigNumber, ): Promise { return this.createAsymmetricFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, fillableAmount, expirationTimeSeconds, ); } public async createFillableSignedOrderWithFeesAsync( makerAssetData: string, takerAssetData: string, makerFee: BigNumber, takerFee: BigNumber, makerAddress: string, takerAddress: string, fillableAmount: BigNumber, feeRecipientAddress: string, expirationTimeSeconds?: BigNumber, ): Promise { return this._createAsymmetricFillableSignedOrderWithFeesAsync( makerAssetData, takerAssetData, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, fillableAmount, feeRecipientAddress, expirationTimeSeconds, ); } public async createAsymmetricFillableSignedOrderAsync( makerAssetData: string, takerAssetData: string, makerAddress: string, takerAddress: string, makerFillableAmount: BigNumber, takerFillableAmount: BigNumber, expirationTimeSeconds?: BigNumber, ): Promise { const makerFee = new BigNumber(0); const takerFee = new BigNumber(0); const feeRecipientAddress = constants.NULL_ADDRESS; return this._createAsymmetricFillableSignedOrderWithFeesAsync( makerAssetData, takerAssetData, makerFee, takerFee, makerAddress, takerAddress, makerFillableAmount, takerFillableAmount, feeRecipientAddress, expirationTimeSeconds, ); } public async createPartiallyFilledSignedOrderAsync( makerAssetData: string, takerAssetData: string, takerAddress: string, fillableAmount: BigNumber, partialFillAmount: BigNumber, ): Promise { const [makerAddress] = this._userAddresses; const signedOrder = await this.createAsymmetricFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, fillableAmount, fillableAmount, ); const exchangeInstance = new ExchangeContract( artifacts.Exchange.compilerOutput.abi, signedOrder.exchangeAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); const orderWithoutExchangeAddress = _.omit(signedOrder, [ 'signature', 'exchangeAddress', ]) as OrderWithoutExchangeAddress; const txHash = await exchangeInstance.fillOrder.sendTransactionAsync( orderWithoutExchangeAddress, partialFillAmount, signedOrder.signature, { from: takerAddress }, ); await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); return signedOrder; } private async _createAsymmetricFillableSignedOrderWithFeesAsync( makerAssetData: string, takerAssetData: string, makerFee: BigNumber, takerFee: BigNumber, makerAddress: string, takerAddress: string, makerFillableAmount: BigNumber, takerFillableAmount: BigNumber, feeRecipientAddress: string, expirationTimeSeconds?: BigNumber, ): Promise { await this._increaseBalanceAndAllowanceWithAssetDataAsync(makerAssetData, makerAddress, makerFillableAmount); await this._increaseBalanceAndAllowanceWithAssetDataAsync(takerAssetData, takerAddress, takerFillableAmount); // Fees await Promise.all([ this._increaseERC20BalanceAndAllowanceAsync(this._zrxTokenAddress, makerAddress, makerFee), this._increaseERC20BalanceAndAllowanceAsync(this._zrxTokenAddress, takerAddress, takerFee), ]); const senderAddress = constants.NULL_ADDRESS; const signedOrder = await orderFactory.createSignedOrderAsync( this._web3Wrapper.getProvider(), makerAddress, makerFillableAmount, makerAssetData, takerFillableAmount, takerAssetData, this._exchangeAddress, { takerAddress, senderAddress, makerFee, takerFee, feeRecipientAddress, expirationTimeSeconds, }, ); return signedOrder; } private async _increaseERC721BalanceAndAllowanceAsync( tokenAddress: string, address: string, tokenId: BigNumber, ): Promise { await this._increaseERC721BalanceAsync(tokenAddress, address, tokenId); await this._increaseERC721AllowanceAsync(tokenAddress, address, tokenId); } private async _increaseERC721AllowanceAsync( tokenAddress: string, address: string, tokenId: BigNumber, ): Promise { const erc721Token = new DummyERC721TokenContract( artifacts.DummyERC721Token.compilerOutput.abi, tokenAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); const txHash = await erc721Token.approve.sendTransactionAsync(this._erc721ProxyAddress, tokenId, { from: address, }); await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); } private async _increaseERC721BalanceAsync( tokenAddress: string, address: string, tokenId: BigNumber, ): Promise { const erc721Token = new DummyERC721TokenContract( artifacts.DummyERC721Token.compilerOutput.abi, tokenAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); try { const currentOwner = await erc721Token.ownerOf.callAsync(tokenId); if (currentOwner !== address) { throw new Error(`Token ${tokenAddress}:${tokenId} is already owner by ${currentOwner}`); } } catch (err) { const txHash = await erc721Token.mint.sendTransactionAsync(address, tokenId, { from: this._coinbase }); await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); } } private async _increaseERC20BalanceAndAllowanceAsync( tokenAddress: string, address: string, amount: BigNumber, ): Promise { if (amount.isZero() || address === constants.NULL_ADDRESS) { return; // noop } await Promise.all([ this._increaseERC20BalanceAsync(tokenAddress, address, amount), this._increaseERC20AllowanceAsync(tokenAddress, address, amount), ]); } private async _increaseERC20BalanceAsync(tokenAddress: string, address: string, amount: BigNumber): Promise { const erc20Token = new DummyERC20TokenContract( artifacts.DummyERC20Token.compilerOutput.abi, tokenAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); const txHash = await erc20Token.transfer.sendTransactionAsync(address, amount, { from: this._coinbase, }); await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); } private async _increaseERC20AllowanceAsync( tokenAddress: string, address: string, amount: BigNumber, ): Promise { const erc20Token = new DummyERC20TokenContract( artifacts.DummyERC20Token.compilerOutput.abi, tokenAddress, this._web3Wrapper.getProvider(), this._web3Wrapper.getContractDefaults(), ); const oldMakerAllowance = await erc20Token.allowance.callAsync(address, this._erc20ProxyAddress); const newMakerAllowance = oldMakerAllowance.plus(amount); const txHash = await erc20Token.approve.sendTransactionAsync(this._erc20ProxyAddress, newMakerAllowance, { from: address, }); await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); } private async _increaseBalanceAndAllowanceWithAssetDataAsync( assetData: string, userAddress: string, amount: BigNumber, ): Promise { const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); if (assetDataUtils.isERC20AssetData(decodedAssetData)) { await this._increaseERC20BalanceAndAllowanceAsync(decodedAssetData.tokenAddress, userAddress, amount); } else if (assetDataUtils.isERC721AssetData(decodedAssetData)) { await this._increaseERC721BalanceAndAllowanceAsync( decodedAssetData.tokenAddress, userAddress, decodedAssetData.tokenId, ); } else if (assetDataUtils.isMultiAssetData(decodedAssetData)) { for (const [index, nestedAssetDataElement] of decodedAssetData.nestedAssetData.entries()) { const amountsElement = decodedAssetData.amounts[index]; const totalAmount = amount.times(amountsElement); await this._increaseBalanceAndAllowanceWithAssetDataAsync( nestedAssetDataElement, userAddress, totalAmount, ); } } } }