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 {
AssetProxyId,
ERC20AssetData,
ERC721AssetData,
MultiAssetData,
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<SignedOrder> {
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<SignedOrder> {
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<SignedOrder> {
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<SignedOrder> {
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<SignedOrder> {
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<void> {
await this._increaseERC721BalanceAsync(tokenAddress, address, tokenId);
await this._increaseERC721AllowanceAsync(tokenAddress, address, tokenId);
}
private async _increaseERC721AllowanceAsync(
tokenAddress: string,
address: string,
tokenId: BigNumber,
): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
switch (decodedAssetData.assetProxyId) {
case AssetProxyId.ERC20:
await this._increaseERC20BalanceAndAllowanceAsync(
(decodedAssetData as ERC20AssetData).tokenAddress,
userAddress,
amount,
);
break;
case AssetProxyId.ERC721:
await this._increaseERC721BalanceAndAllowanceAsync(
(decodedAssetData as ERC721AssetData).tokenAddress,
userAddress,
// tslint:disable-next-line:no-unnecessary-type-assertion
(decodedAssetData as ERC721AssetData).tokenId,
);
break;
case AssetProxyId.MultiAsset:
for (const [
index,
nestedAssetDataElement,
] of (decodedAssetData as MultiAssetData).nestedAssetData.entries()) {
const amountsElement = (decodedAssetData as MultiAssetData).amounts[index];
const totalAmount = amount.times(amountsElement);
await this._increaseBalanceAndAllowanceWithAssetDataAsync(
nestedAssetDataElement,
userAddress,
totalAmount,
);
}
break;
default:
throw new Error(`Proxy with id ${decodedAssetData.assetProxyId} not supported`);
}
}
}