Split protocol package into exchange, asset-proxy, and multisig

This commit is contained in:
Amir Bandeali
2019-01-20 21:21:44 -08:00
parent e2fe907de0
commit 4a4c26a2e3
85 changed files with 2253 additions and 127 deletions

View File

@@ -0,0 +1,222 @@
import { AbstractAssetWrapper, constants } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber, errorUtils } from '@0x/utils';
import * as _ from 'lodash';
import { ERC20Wrapper } from './erc20_wrapper';
import { ERC721Wrapper } from './erc721_wrapper';
interface ProxyIdToAssetWrappers {
[proxyId: string]: AbstractAssetWrapper;
}
/**
* This class abstracts away the differences between ERC20 and ERC721 tokens so that
* the logic that uses it does not need to care what standard a token belongs to.
*/
export class AssetWrapper {
private readonly _proxyIdToAssetWrappers: ProxyIdToAssetWrappers;
constructor(assetWrappers: AbstractAssetWrapper[]) {
this._proxyIdToAssetWrappers = {};
_.each(assetWrappers, assetWrapper => {
const proxyId = assetWrapper.getProxyId();
this._proxyIdToAssetWrappers[proxyId] = assetWrapper;
});
}
public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
switch (proxyId) {
case AssetProxyId.ERC20: {
const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
const balance = await erc20Wrapper.getBalanceAsync(userAddress, assetData);
return balance;
}
case AssetProxyId.ERC721: {
const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
const isOwner = await assetWrapper.isOwnerAsync(
userAddress,
assetProxyData.tokenAddress,
assetProxyData.tokenId,
);
const balance = isOwner ? new BigNumber(1) : new BigNumber(0);
return balance;
}
default:
throw errorUtils.spawnSwitchErr('proxyId', proxyId);
}
}
public async setBalanceAsync(userAddress: string, assetData: string, desiredBalance: BigNumber): Promise<void> {
const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
switch (proxyId) {
case AssetProxyId.ERC20: {
const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
await erc20Wrapper.setBalanceAsync(userAddress, assetData, desiredBalance);
return;
}
case AssetProxyId.ERC721: {
if (!desiredBalance.eq(0) && !desiredBalance.eq(1)) {
throw new Error(`Balance for ERC721 token can only be set to 0 or 1. Got: ${desiredBalance}`);
}
const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
const doesTokenExist = erc721Wrapper.doesTokenExistAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
);
if (!doesTokenExist && desiredBalance.eq(1)) {
await erc721Wrapper.mintAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress);
return;
} else if (!doesTokenExist && desiredBalance.eq(0)) {
return; // noop
}
const tokenOwner = await erc721Wrapper.ownerOfAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
);
if (userAddress !== tokenOwner && desiredBalance.eq(1)) {
await erc721Wrapper.transferFromAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
tokenOwner,
userAddress,
);
} else if (tokenOwner === userAddress && desiredBalance.eq(0)) {
// Transfer token to someone else
const userAddresses = await (erc721Wrapper as any)._web3Wrapper.getAvailableAddressesAsync();
const nonOwner = _.find(userAddresses, a => a !== userAddress);
await erc721Wrapper.transferFromAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
tokenOwner,
nonOwner,
);
return;
} else if (
(userAddress !== tokenOwner && desiredBalance.eq(0)) ||
(tokenOwner === userAddress && desiredBalance.eq(1))
) {
return; // noop
}
break;
}
default:
throw errorUtils.spawnSwitchErr('proxyId', proxyId);
}
}
public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
switch (proxyId) {
case AssetProxyId.ERC20: {
const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
const allowance = await erc20Wrapper.getProxyAllowanceAsync(userAddress, assetData);
return allowance;
}
case AssetProxyId.ERC721: {
const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
const erc721ProxyData = assetDataUtils.decodeERC721AssetData(assetData);
const isProxyApprovedForAll = await assetWrapper.isProxyApprovedForAllAsync(
userAddress,
erc721ProxyData.tokenAddress,
);
if (isProxyApprovedForAll) {
return constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
}
const isProxyApproved = await assetWrapper.isProxyApprovedAsync(
erc721ProxyData.tokenAddress,
erc721ProxyData.tokenId,
);
const allowance = isProxyApproved ? new BigNumber(1) : new BigNumber(0);
return allowance;
}
default:
throw errorUtils.spawnSwitchErr('proxyId', proxyId);
}
}
public async setProxyAllowanceAsync(
userAddress: string,
assetData: string,
desiredAllowance: BigNumber,
): Promise<void> {
const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
switch (proxyId) {
case AssetProxyId.ERC20: {
const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
await erc20Wrapper.setAllowanceAsync(userAddress, assetData, desiredAllowance);
return;
}
case AssetProxyId.ERC721: {
if (
!desiredAllowance.eq(0) &&
!desiredAllowance.eq(1) &&
!desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
) {
throw new Error(
`Allowance for ERC721 token can only be set to 0, 1 or 2^256-1. Got: ${desiredAllowance}`,
);
}
const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
const doesTokenExist = await erc721Wrapper.doesTokenExistAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
);
if (!doesTokenExist) {
throw new Error(
`Cannot setProxyAllowance on non-existent token: ${assetProxyData.tokenAddress} ${
assetProxyData.tokenId
}`,
);
}
const isProxyApprovedForAll = await erc721Wrapper.isProxyApprovedForAllAsync(
userAddress,
assetProxyData.tokenAddress,
);
if (!isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
const isApproved = true;
await erc721Wrapper.approveProxyForAllAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
isApproved,
);
} else if (isProxyApprovedForAll && desiredAllowance.eq(0)) {
const isApproved = false;
await erc721Wrapper.approveProxyForAllAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
isApproved,
);
} else if (isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
return; // Noop
}
const isProxyApproved = await erc721Wrapper.isProxyApprovedAsync(
assetProxyData.tokenAddress,
assetProxyData.tokenId,
);
if (!isProxyApproved && desiredAllowance.eq(1)) {
await erc721Wrapper.approveProxyAsync(assetProxyData.tokenAddress, assetProxyData.tokenId);
} else if (isProxyApproved && desiredAllowance.eq(0)) {
// Remove approval
await erc721Wrapper.approveAsync(
constants.NULL_ADDRESS,
assetProxyData.tokenAddress,
assetProxyData.tokenId,
);
} else if (
(!isProxyApproved && desiredAllowance.eq(0)) ||
(isProxyApproved && desiredAllowance.eq(1))
) {
return; // noop
}
break;
}
default:
throw errorUtils.spawnSwitchErr('proxyId', proxyId);
}
}
}

View File

@@ -0,0 +1,177 @@
import { artifacts as proxyArtifacts, ERC20ProxyContract } from '@0x/contracts-asset-proxy';
import { constants, ERC20BalancesByOwner, txDefaults } from '@0x/contracts-test-utils';
import { artifacts as tokensArtifacts, DummyERC20TokenContract } from '@0x/contracts-tokens';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
export class ERC20Wrapper {
private readonly _tokenOwnerAddresses: string[];
private readonly _contractOwnerAddress: string;
private readonly _web3Wrapper: Web3Wrapper;
private readonly _provider: Provider;
private readonly _dummyTokenContracts: DummyERC20TokenContract[];
private _proxyContract?: ERC20ProxyContract;
private _proxyIdIfExists?: string;
/**
* Instanitates an ERC20Wrapper
* @param provider Web3 provider to use for all JSON RPC requests
* @param tokenOwnerAddresses Addresses that we want to endow as owners for dummy ERC20 tokens
* @param contractOwnerAddress Desired owner of the contract
* Instance of ERC20Wrapper
*/
constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
this._dummyTokenContracts = [];
this._web3Wrapper = new Web3Wrapper(provider);
this._provider = provider;
this._tokenOwnerAddresses = tokenOwnerAddresses;
this._contractOwnerAddress = contractOwnerAddress;
}
public async deployDummyTokensAsync(
numberToDeploy: number,
decimals: BigNumber,
): Promise<DummyERC20TokenContract[]> {
for (let i = 0; i < numberToDeploy; i++) {
this._dummyTokenContracts.push(
await DummyERC20TokenContract.deployFrom0xArtifactAsync(
tokensArtifacts.DummyERC20Token,
this._provider,
txDefaults,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
decimals,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
),
);
}
return this._dummyTokenContracts;
}
public async deployProxyAsync(): Promise<ERC20ProxyContract> {
this._proxyContract = await ERC20ProxyContract.deployFrom0xArtifactAsync(
proxyArtifacts.ERC20Proxy,
this._provider,
txDefaults,
);
this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync();
return this._proxyContract;
}
public getProxyId(): string {
this._validateProxyContractExistsOrThrow();
return this._proxyIdIfExists as string;
}
public async setBalancesAndAllowancesAsync(): Promise<void> {
this._validateDummyTokenContractsExistOrThrow();
this._validateProxyContractExistsOrThrow();
for (const dummyTokenContract of this._dummyTokenContracts) {
for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
await this._web3Wrapper.awaitTransactionSuccessAsync(
await dummyTokenContract.setBalance.sendTransactionAsync(
tokenOwnerAddress,
constants.INITIAL_ERC20_BALANCE,
{ from: this._contractOwnerAddress },
),
constants.AWAIT_TRANSACTION_MINED_MS,
);
await this._web3Wrapper.awaitTransactionSuccessAsync(
await dummyTokenContract.approve.sendTransactionAsync(
(this._proxyContract as ERC20ProxyContract).address,
constants.INITIAL_ERC20_ALLOWANCE,
{ from: tokenOwnerAddress },
),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
}
}
public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
const tokenContract = this._getTokenContractFromAssetData(assetData);
const balance = new BigNumber(await tokenContract.balanceOf.callAsync(userAddress));
return balance;
}
public async setBalanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(assetData);
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.setBalance.sendTransactionAsync(userAddress, amount, {
from: this._contractOwnerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
const tokenContract = this._getTokenContractFromAssetData(assetData);
const proxyAddress = (this._proxyContract as ERC20ProxyContract).address;
const allowance = new BigNumber(await tokenContract.allowance.callAsync(userAddress, proxyAddress));
return allowance;
}
public async setAllowanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(assetData);
const proxyAddress = (this._proxyContract as ERC20ProxyContract).address;
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.approve.sendTransactionAsync(proxyAddress, amount, {
from: userAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async getBalancesAsync(): Promise<ERC20BalancesByOwner> {
this._validateDummyTokenContractsExistOrThrow();
const balancesByOwner: ERC20BalancesByOwner = {};
const balances: BigNumber[] = [];
const balanceInfo: Array<{ tokenOwnerAddress: string; tokenAddress: string }> = [];
for (const dummyTokenContract of this._dummyTokenContracts) {
for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
balances.push(await dummyTokenContract.balanceOf.callAsync(tokenOwnerAddress));
balanceInfo.push({
tokenOwnerAddress,
tokenAddress: dummyTokenContract.address,
});
}
}
_.forEach(balances, (balance, balanceIndex) => {
const tokenAddress = balanceInfo[balanceIndex].tokenAddress;
const tokenOwnerAddress = balanceInfo[balanceIndex].tokenOwnerAddress;
if (_.isUndefined(balancesByOwner[tokenOwnerAddress])) {
balancesByOwner[tokenOwnerAddress] = {};
}
const wrappedBalance = new BigNumber(balance);
balancesByOwner[tokenOwnerAddress][tokenAddress] = wrappedBalance;
});
return balancesByOwner;
}
public addDummyTokenContract(dummy: DummyERC20TokenContract): void {
if (!_.isUndefined(this._dummyTokenContracts)) {
this._dummyTokenContracts.push(dummy);
}
}
public addTokenOwnerAddress(address: string): void {
this._tokenOwnerAddresses.push(address);
}
public getTokenOwnerAddresses(): string[] {
return this._tokenOwnerAddresses;
}
public getTokenAddresses(): string[] {
const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
return tokenAddresses;
}
private _getTokenContractFromAssetData(assetData: string): DummyERC20TokenContract {
const erc20ProxyData = assetDataUtils.decodeERC20AssetData(assetData);
const tokenAddress = erc20ProxyData.tokenAddress;
const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress);
if (_.isUndefined(tokenContractIfExists)) {
throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`);
}
return tokenContractIfExists;
}
private _validateDummyTokenContractsExistOrThrow(): void {
if (_.isUndefined(this._dummyTokenContracts)) {
throw new Error('Dummy ERC20 tokens not yet deployed, please call "deployDummyTokensAsync"');
}
}
private _validateProxyContractExistsOrThrow(): void {
if (_.isUndefined(this._proxyContract)) {
throw new Error('ERC20 proxy contract not yet deployed, please call "deployProxyAsync"');
}
}
}

View File

@@ -0,0 +1,234 @@
import { artifacts as proxyArtifacts, ERC721ProxyContract } from '@0x/contracts-asset-proxy';
import { constants, ERC721TokenIdsByOwner, txDefaults } from '@0x/contracts-test-utils';
import { artifacts as tokensArtifacts, DummyERC721TokenContract } from '@0x/contracts-tokens';
import { generatePseudoRandomSalt } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
export class ERC721Wrapper {
private readonly _tokenOwnerAddresses: string[];
private readonly _contractOwnerAddress: string;
private readonly _web3Wrapper: Web3Wrapper;
private readonly _provider: Provider;
private readonly _dummyTokenContracts: DummyERC721TokenContract[];
private _proxyContract?: ERC721ProxyContract;
private _proxyIdIfExists?: string;
private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {};
constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
this._web3Wrapper = new Web3Wrapper(provider);
this._provider = provider;
this._dummyTokenContracts = [];
this._tokenOwnerAddresses = tokenOwnerAddresses;
this._contractOwnerAddress = contractOwnerAddress;
}
public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> {
// tslint:disable-next-line:no-unused-variable
for (const i of _.times(constants.NUM_DUMMY_ERC721_TO_DEPLOY)) {
this._dummyTokenContracts.push(
await DummyERC721TokenContract.deployFrom0xArtifactAsync(
tokensArtifacts.DummyERC721Token,
this._provider,
txDefaults,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
),
);
}
return this._dummyTokenContracts;
}
public async deployProxyAsync(): Promise<ERC721ProxyContract> {
this._proxyContract = await ERC721ProxyContract.deployFrom0xArtifactAsync(
proxyArtifacts.ERC721Proxy,
this._provider,
txDefaults,
);
this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync();
return this._proxyContract;
}
public getProxyId(): string {
this._validateProxyContractExistsOrThrow();
return this._proxyIdIfExists as string;
}
public async setBalancesAndAllowancesAsync(): Promise<void> {
this._validateDummyTokenContractsExistOrThrow();
this._validateProxyContractExistsOrThrow();
this._initialTokenIdsByOwner = {};
for (const dummyTokenContract of this._dummyTokenContracts) {
for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
// tslint:disable-next-line:no-unused-variable
for (const i of _.times(constants.NUM_ERC721_TOKENS_TO_MINT)) {
const tokenId = generatePseudoRandomSalt();
await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress);
if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) {
this._initialTokenIdsByOwner[tokenOwnerAddress] = {
[dummyTokenContract.address]: [],
};
}
if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address])) {
this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address] = [];
}
this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address].push(tokenId);
await this.approveProxyAsync(dummyTokenContract.address, tokenId);
}
}
}
}
public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const owner = await tokenContract.ownerOf.callAsync(tokenId);
const doesExist = owner !== constants.NULL_ADDRESS;
return doesExist;
}
public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber): Promise<void> {
const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
await this.approveAsync(proxyAddress, tokenAddress, tokenId);
}
public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId);
const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.setApprovalForAll.sendTransactionAsync(proxyAddress, isApproved, {
from: tokenOwner,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async approveAsync(to: string, tokenAddress: string, tokenId: BigNumber): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId);
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.approve.sendTransactionAsync(to, tokenId, {
from: tokenOwner,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async transferFromAsync(
tokenAddress: string,
tokenId: BigNumber,
currentOwner: string,
userAddress: string,
): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.transferFrom.sendTransactionAsync(currentOwner, userAddress, tokenId, {
from: currentOwner,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async mintAsync(tokenAddress: string, tokenId: BigNumber, userAddress: string): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.mint.sendTransactionAsync(userAddress, tokenId, {
from: this._contractOwnerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async burnAsync(tokenAddress: string, tokenId: BigNumber, owner: string): Promise<void> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
await this._web3Wrapper.awaitTransactionSuccessAsync(
await tokenContract.burn.sendTransactionAsync(owner, tokenId, {
from: this._contractOwnerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
public async ownerOfAsync(tokenAddress: string, tokenId: BigNumber): Promise<string> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const owner = await tokenContract.ownerOf.callAsync(tokenId);
return owner;
}
public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId);
const isOwner = tokenOwner === userAddress;
return isOwner;
}
public async isProxyApprovedForAllAsync(userAddress: string, tokenAddress: string): Promise<boolean> {
this._validateProxyContractExistsOrThrow();
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const operator = (this._proxyContract as ERC721ProxyContract).address;
const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator);
return didApproveAll;
}
public async isProxyApprovedAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
this._validateProxyContractExistsOrThrow();
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const approvedAddress = await tokenContract.getApproved.callAsync(tokenId);
const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
const isProxyAnApprovedOperator = approvedAddress === proxyAddress;
return isProxyAnApprovedOperator;
}
public async getBalancesAsync(): Promise<ERC721TokenIdsByOwner> {
this._validateDummyTokenContractsExistOrThrow();
this._validateBalancesAndAllowancesSetOrThrow();
const tokenIdsByOwner: ERC721TokenIdsByOwner = {};
const tokenOwnerAddresses: string[] = [];
const tokenInfo: Array<{ tokenId: BigNumber; tokenAddress: string }> = [];
for (const dummyTokenContract of this._dummyTokenContracts) {
for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
const initialTokenOwnerIds = this._initialTokenIdsByOwner[tokenOwnerAddress][
dummyTokenContract.address
];
for (const tokenId of initialTokenOwnerIds) {
tokenOwnerAddresses.push(await dummyTokenContract.ownerOf.callAsync(tokenId));
tokenInfo.push({
tokenId,
tokenAddress: dummyTokenContract.address,
});
}
}
}
_.forEach(tokenOwnerAddresses, (tokenOwnerAddress, ownerIndex) => {
const tokenAddress = tokenInfo[ownerIndex].tokenAddress;
const tokenId = tokenInfo[ownerIndex].tokenId;
if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress])) {
tokenIdsByOwner[tokenOwnerAddress] = {
[tokenAddress]: [],
};
}
if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress][tokenAddress])) {
tokenIdsByOwner[tokenOwnerAddress][tokenAddress] = [];
}
tokenIdsByOwner[tokenOwnerAddress][tokenAddress].push(tokenId);
});
return tokenIdsByOwner;
}
public getTokenOwnerAddresses(): string[] {
return this._tokenOwnerAddresses;
}
public getTokenAddresses(): string[] {
const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
return tokenAddresses;
}
private _getTokenContractFromAssetData(tokenAddress: string): DummyERC721TokenContract {
const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress);
if (_.isUndefined(tokenContractIfExists)) {
throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`);
}
return tokenContractIfExists;
}
private _validateDummyTokenContractsExistOrThrow(): void {
if (_.isUndefined(this._dummyTokenContracts)) {
throw new Error('Dummy ERC721 tokens not yet deployed, please call "deployDummyTokensAsync"');
}
}
private _validateProxyContractExistsOrThrow(): void {
if (_.isUndefined(this._proxyContract)) {
throw new Error('ERC721 proxy contract not yet deployed, please call "deployProxyAsync"');
}
}
private _validateBalancesAndAllowancesSetOrThrow(): void {
if (_.keys(this._initialTokenIdsByOwner).length === 0) {
throw new Error(
'Dummy ERC721 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"',
);
}
}
}

View File

@@ -0,0 +1,292 @@
import {
FillResults,
formatters,
LogDecoder,
OrderInfo,
orderUtils,
SignedTransaction,
} from '@0x/contracts-test-utils';
import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
import { SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { MethodAbi, Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { ExchangeContract } from '../../generated-wrappers/exchange';
import { artifacts } from '../../src/artifacts';
import { AbiDecodedFillOrderData } from './types';
export class ExchangeWrapper {
private readonly _exchange: ExchangeContract;
private readonly _web3Wrapper: Web3Wrapper;
private readonly _logDecoder: LogDecoder;
constructor(exchangeContract: ExchangeContract, provider: Provider) {
this._exchange = exchangeContract;
this._web3Wrapper = new Web3Wrapper(provider);
this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts });
}
public async fillOrderAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const txHash = await this._exchange.fillOrder.sendTransactionAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return txReceipt;
}
public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createCancel(signedOrder);
const txHash = await this._exchange.cancelOrder.sendTransactionAsync(params.order, { from });
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async fillOrKillOrderAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const txHash = await this._exchange.fillOrKillOrder.sendTransactionAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async fillOrderNoThrowAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const txHash = await this._exchange.fillOrderNoThrow.sendTransactionAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from, gas: opts.gas },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async batchFillOrdersAsync(
orders: SignedOrder[],
from: string,
opts: { takerAssetFillAmounts?: BigNumber[] } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
const txHash = await this._exchange.batchFillOrders.sendTransactionAsync(
params.orders,
params.takerAssetFillAmounts,
params.signatures,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async batchFillOrKillOrdersAsync(
orders: SignedOrder[],
from: string,
opts: { takerAssetFillAmounts?: BigNumber[] } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
const txHash = await this._exchange.batchFillOrKillOrders.sendTransactionAsync(
params.orders,
params.takerAssetFillAmounts,
params.signatures,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async batchFillOrdersNoThrowAsync(
orders: SignedOrder[],
from: string,
opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
const txHash = await this._exchange.batchFillOrdersNoThrow.sendTransactionAsync(
params.orders,
params.takerAssetFillAmounts,
params.signatures,
{ from, gas: opts.gas },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async marketSellOrdersAsync(
orders: SignedOrder[],
from: string,
opts: { takerAssetFillAmount: BigNumber },
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
const txHash = await this._exchange.marketSellOrders.sendTransactionAsync(
params.orders,
params.takerAssetFillAmount,
params.signatures,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async marketSellOrdersNoThrowAsync(
orders: SignedOrder[],
from: string,
opts: { takerAssetFillAmount: BigNumber; gas?: number },
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
const txHash = await this._exchange.marketSellOrdersNoThrow.sendTransactionAsync(
params.orders,
params.takerAssetFillAmount,
params.signatures,
{ from, gas: opts.gas },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async marketBuyOrdersAsync(
orders: SignedOrder[],
from: string,
opts: { makerAssetFillAmount: BigNumber },
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
const txHash = await this._exchange.marketBuyOrders.sendTransactionAsync(
params.orders,
params.makerAssetFillAmount,
params.signatures,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async marketBuyOrdersNoThrowAsync(
orders: SignedOrder[],
from: string,
opts: { makerAssetFillAmount: BigNumber; gas?: number },
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
const txHash = await this._exchange.marketBuyOrdersNoThrow.sendTransactionAsync(
params.orders,
params.makerAssetFillAmount,
params.signatures,
{ from, gas: opts.gas },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async batchCancelOrdersAsync(
orders: SignedOrder[],
from: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const params = formatters.createBatchCancel(orders);
const txHash = await this._exchange.batchCancelOrders.sendTransactionAsync(params.orders, { from });
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
const txHash = await this._exchange.cancelOrdersUpTo.sendTransactionAsync(salt, { from });
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async registerAssetProxyAsync(
assetProxyAddress: string,
from: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const txHash = await this._exchange.registerAssetProxy.sendTransactionAsync(assetProxyAddress, { from });
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async executeTransactionAsync(
signedTx: SignedTransaction,
from: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const txHash = await this._exchange.executeTransaction.sendTransactionAsync(
signedTx.salt,
signedTx.signerAddress,
signedTx.data,
signedTx.signature,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> {
const filledAmount = await this._exchange.filled.callAsync(orderHashHex);
return filledAmount;
}
public async isCancelledAsync(orderHashHex: string): Promise<boolean> {
const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex);
return isCancelled;
}
public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise<BigNumber> {
const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress);
return orderEpoch;
}
public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> {
const orderInfo = await this._exchange.getOrderInfo.callAsync(signedOrder);
return orderInfo;
}
public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> {
const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[];
return ordersInfo;
}
public async matchOrdersAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
from: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight);
const txHash = await this._exchange.matchOrders.sendTransactionAsync(
params.left,
params.right,
params.leftSignature,
params.rightSignature,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async getFillOrderResultsAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<FillResults> {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const fillResults = await this._exchange.fillOrder.callAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
return fillResults;
}
public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const data = this._exchange.fillOrder.getABIEncodedTransactionData(
params.order,
params.takerAssetFillAmount,
params.signature,
);
return data;
}
public abiDecodeFillOrder(data: string): AbiDecodedFillOrderData {
// Lookup fillOrder ABI in exchange abi
const fillOrderAbi = _.find(this._exchange.abi, { name: 'fillOrder' }) as MethodAbi;
// Decode input data
const abiEncoder = new AbiEncoder.Method(fillOrderAbi);
const decodedData = abiEncoder.decode(data) as AbiDecodedFillOrderData;
return decodedData;
}
public getExchangeAddress(): string {
return this._exchange.address;
}
}

View File

@@ -0,0 +1,928 @@
import { artifacts as libsArtifacts, TestLibsContract } from '@0x/contracts-libs';
import {
AllowanceAmountScenario,
AssetDataScenario,
BalanceAmountScenario,
chaiSetup,
constants,
expectTransactionFailedAsync,
ExpirationTimeSecondsScenario,
FeeRecipientAddressScenario,
FillScenario,
OrderAssetAmountScenario,
orderUtils,
signingUtils,
TakerAssetFillAmountScenario,
TakerScenario,
TraderStateScenario,
} from '@0x/contracts-test-utils';
import {
assetDataUtils,
BalanceAndProxyAllowanceLazyStore,
ExchangeTransferSimulator,
orderHashUtils,
OrderStateUtils,
OrderValidationUtils,
} from '@0x/order-utils';
import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0x/types';
import { BigNumber, errorUtils, logUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai';
import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import 'make-promises-safe';
import { ExchangeContract, ExchangeFillEventArgs } from '../../generated-wrappers/exchange';
import { artifacts } from '../../src/artifacts';
import { AssetWrapper } from './asset_wrapper';
import { ERC20Wrapper } from './erc20_wrapper';
import { ERC721Wrapper } from './erc721_wrapper';
import { ExchangeWrapper } from './exchange_wrapper';
import { OrderFactoryFromScenario } from './order_factory_from_scenario';
import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher';
import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher';
chaiSetup.configure();
const expect = chai.expect;
/**
* Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some
* required async setup, a factory method is required.
* @param web3Wrapper Web3Wrapper instance
* @param txDefaults Default Ethereum tx options
* @return FillOrderCombinatorialUtils instance
*/
export async function fillOrderCombinatorialUtilsFactoryAsync(
web3Wrapper: Web3Wrapper,
txDefaults: Partial<TxData>,
): Promise<FillOrderCombinatorialUtils> {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const userAddresses = _.slice(accounts, 0, 5);
const [ownerAddress, makerAddress, takerAddress] = userAddresses;
const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
const provider = web3Wrapper.getProvider();
const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress);
const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress);
const erc20EighteenDecimalTokenCount = 3;
const eighteenDecimals = new BigNumber(18);
const [
erc20EighteenDecimalTokenA,
erc20EighteenDecimalTokenB,
zrxToken,
] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals);
const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
const erc20FiveDecimalTokenCount = 2;
const fiveDecimals = new BigNumber(5);
const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
erc20FiveDecimalTokenCount,
fiveDecimals,
);
const zeroDecimals = new BigNumber(0);
const erc20ZeroDecimalTokenCount = 2;
const [erc20ZeroDecimalTokenA, erc20ZeroDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
erc20ZeroDecimalTokenCount,
zeroDecimals,
);
const erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
const erc721Proxy = await erc721Wrapper.deployProxyAsync();
await erc721Wrapper.setBalancesAndAllowancesAsync();
const erc721Balances = await erc721Wrapper.getBalancesAsync();
const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]);
const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync(
artifacts.Exchange,
provider,
txDefaults,
zrxAssetData,
);
const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider);
await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress);
await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress);
await web3Wrapper.awaitTransactionSuccessAsync(
await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
from: ownerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
await web3Wrapper.awaitTransactionSuccessAsync(
await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
from: ownerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const orderFactory = new OrderFactoryFromScenario(
userAddresses,
zrxToken.address,
[erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address],
[erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address],
[erc20ZeroDecimalTokenA.address, erc20ZeroDecimalTokenB.address],
erc721Token,
erc721Balances,
exchangeContract.address,
);
const testLibsContract = await TestLibsContract.deployFrom0xArtifactAsync(
libsArtifacts.TestLibs,
provider,
txDefaults,
);
const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils(
orderFactory,
ownerAddress,
makerAddress,
makerPrivateKey,
takerAddress,
zrxAssetData,
exchangeWrapper,
assetWrapper,
testLibsContract,
);
return fillOrderCombinatorialUtils;
}
export class FillOrderCombinatorialUtils {
public orderFactory: OrderFactoryFromScenario;
public ownerAddress: string;
public makerAddress: string;
public makerPrivateKey: Buffer;
public takerAddress: string;
public zrxAssetData: string;
public exchangeWrapper: ExchangeWrapper;
public assetWrapper: AssetWrapper;
public testLibsContract: TestLibsContract;
public static generateFillOrderCombinations(): FillScenario[] {
const takerScenarios = [
TakerScenario.Unspecified,
// TakerScenario.CorrectlySpecified,
// TakerScenario.IncorrectlySpecified,
];
const feeRecipientScenarios = [
FeeRecipientAddressScenario.EthUserAddress,
// FeeRecipientAddressScenario.BurnAddress,
];
const makerAssetAmountScenario = [
OrderAssetAmountScenario.Large,
// OrderAssetAmountScenario.Zero,
// OrderAssetAmountScenario.Small,
];
const takerAssetAmountScenario = [
OrderAssetAmountScenario.Large,
// OrderAssetAmountScenario.Zero,
// OrderAssetAmountScenario.Small,
];
const makerFeeScenario = [
OrderAssetAmountScenario.Large,
// OrderAssetAmountScenario.Small,
// OrderAssetAmountScenario.Zero,
];
const takerFeeScenario = [
OrderAssetAmountScenario.Large,
// OrderAssetAmountScenario.Small,
// OrderAssetAmountScenario.Zero,
];
const expirationTimeSecondsScenario = [
ExpirationTimeSecondsScenario.InFuture,
ExpirationTimeSecondsScenario.InPast,
];
const makerAssetDataScenario = [
AssetDataScenario.ERC20FiveDecimals,
AssetDataScenario.ERC20NonZRXEighteenDecimals,
AssetDataScenario.ERC721,
AssetDataScenario.ZRXFeeToken,
];
const takerAssetDataScenario = [
AssetDataScenario.ERC20FiveDecimals,
AssetDataScenario.ERC20NonZRXEighteenDecimals,
AssetDataScenario.ERC721,
AssetDataScenario.ZRXFeeToken,
];
const takerAssetFillAmountScenario = [
TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
// TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
// TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
];
const makerAssetBalanceScenario = [
BalanceAmountScenario.Higher,
// BalanceAmountScenario.Exact,
// BalanceAmountScenario.TooLow,
];
const makerAssetAllowanceScenario = [
AllowanceAmountScenario.Higher,
// AllowanceAmountScenario.Exact,
// AllowanceAmountScenario.TooLow,
// AllowanceAmountScenario.Unlimited,
];
const makerZRXBalanceScenario = [
BalanceAmountScenario.Higher,
// BalanceAmountScenario.Exact,
// BalanceAmountScenario.TooLow,
];
const makerZRXAllowanceScenario = [
AllowanceAmountScenario.Higher,
// AllowanceAmountScenario.Exact,
// AllowanceAmountScenario.TooLow,
// AllowanceAmountScenario.Unlimited,
];
const takerAssetBalanceScenario = [
BalanceAmountScenario.Higher,
// BalanceAmountScenario.Exact,
// BalanceAmountScenario.TooLow,
];
const takerAssetAllowanceScenario = [
AllowanceAmountScenario.Higher,
// AllowanceAmountScenario.Exact,
// AllowanceAmountScenario.TooLow,
// AllowanceAmountScenario.Unlimited,
];
const takerZRXBalanceScenario = [
BalanceAmountScenario.Higher,
// BalanceAmountScenario.Exact,
// BalanceAmountScenario.TooLow,
];
const takerZRXAllowanceScenario = [
AllowanceAmountScenario.Higher,
// AllowanceAmountScenario.Exact,
// AllowanceAmountScenario.TooLow,
// AllowanceAmountScenario.Unlimited,
];
const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([
takerScenarios,
feeRecipientScenarios,
makerAssetAmountScenario,
takerAssetAmountScenario,
makerFeeScenario,
takerFeeScenario,
expirationTimeSecondsScenario,
makerAssetDataScenario,
takerAssetDataScenario,
takerAssetFillAmountScenario,
makerAssetBalanceScenario,
makerAssetAllowanceScenario,
makerZRXBalanceScenario,
makerZRXAllowanceScenario,
takerAssetBalanceScenario,
takerAssetAllowanceScenario,
takerZRXBalanceScenario,
takerZRXAllowanceScenario,
]);
const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => {
// tslint:disable:custom-no-magic-numbers
const fillScenario: FillScenario = {
orderScenario: {
takerScenario: fillScenarioArray[0] as TakerScenario,
feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario,
makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario,
takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario,
makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario,
takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario,
expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario,
makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario,
takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario,
},
takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario,
makerStateScenario: {
traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario,
traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario,
zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario,
zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario,
},
takerStateScenario: {
traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario,
traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario,
zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario,
zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario,
},
};
// tslint:enable:custom-no-magic-numbers
return fillScenario;
});
return fillScenarios;
}
/**
* Recursive implementation of generating all combinations of the supplied
* string-containing arrays.
*/
private static _getAllCombinations(arrays: string[][]): string[][] {
// Base case
if (arrays.length === 1) {
const remainingValues = _.map(arrays[0], val => {
return [val];
});
return remainingValues;
} else {
const result = [];
const restOfArrays = arrays.slice(1);
const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array
// tslint:disable:prefer-for-of
for (let i = 0; i < allCombinationsOfRemaining.length; i++) {
for (let j = 0; j < arrays[0].length; j++) {
result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]);
}
}
// tslint:enable:prefer-for-of
return result;
}
}
constructor(
orderFactory: OrderFactoryFromScenario,
ownerAddress: string,
makerAddress: string,
makerPrivateKey: Buffer,
takerAddress: string,
zrxAssetData: string,
exchangeWrapper: ExchangeWrapper,
assetWrapper: AssetWrapper,
testLibsContract: TestLibsContract,
) {
this.orderFactory = orderFactory;
this.ownerAddress = ownerAddress;
this.makerAddress = makerAddress;
this.makerPrivateKey = makerPrivateKey;
this.takerAddress = takerAddress;
this.zrxAssetData = zrxAssetData;
this.exchangeWrapper = exchangeWrapper;
this.assetWrapper = assetWrapper;
this.testLibsContract = testLibsContract;
}
public async testFillOrderScenarioAsync(
provider: Provider,
fillScenario: FillScenario,
isVerbose: boolean = false,
): Promise<void> {
// 1. Generate order
const order = this.orderFactory.generateOrder(fillScenario.orderScenario);
// 2. Sign order
const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign);
const signedOrder = {
...order,
signature: `0x${signature.toString('hex')}`,
};
const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper);
const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(
this.exchangeWrapper,
this.zrxAssetData,
);
// 3. Figure out fill amount
const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync(
signedOrder,
fillScenario.takerAssetFillAmountScenario,
balanceAndProxyAllowanceFetcher,
orderFilledCancelledFetcher,
);
// 4. Permutate the maker and taker balance/allowance scenarios
await this._modifyTraderStateAsync(
fillScenario.makerStateScenario,
fillScenario.takerStateScenario,
signedOrder,
takerAssetFillAmount,
);
// 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected?
const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher, provider);
const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher);
const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore);
let fillRevertReasonIfExists;
try {
await orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
exchangeTransferSimulator,
provider,
signedOrder,
takerAssetFillAmount,
this.takerAddress,
this.zrxAssetData,
);
if (isVerbose) {
logUtils.log(`Expecting fillOrder to succeed.`);
}
} catch (err) {
fillRevertReasonIfExists = err.message;
if (isVerbose) {
logUtils.log(`Expecting fillOrder to fail with:`);
logUtils.log(err);
}
}
// 6. Fill the order
await this._fillOrderAndAssertOutcomeAsync(
signedOrder,
takerAssetFillAmount,
lazyStore,
fillRevertReasonIfExists,
);
await this._abiEncodeFillOrderAndAssertOutcomeAsync(signedOrder, takerAssetFillAmount);
}
private async _fillOrderAndAssertOutcomeAsync(
signedOrder: SignedOrder,
takerAssetFillAmount: BigNumber,
lazyStore: BalanceAndProxyAllowanceLazyStore,
fillRevertReasonIfExists: RevertReason | undefined,
): Promise<void> {
if (!_.isUndefined(fillRevertReasonIfExists)) {
return expectTransactionFailedAsync(
this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }),
fillRevertReasonIfExists,
);
}
const makerAddress = signedOrder.makerAddress;
const makerAssetData = signedOrder.makerAssetData;
const takerAssetData = signedOrder.takerAssetData;
const feeRecipient = signedOrder.feeRecipientAddress;
const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress);
const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress);
const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress);
const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress);
const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress);
const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress);
const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress);
const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress);
const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress);
const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(
this.zrxAssetData,
this.takerAddress,
);
const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient);
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount);
const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill)
? remainingTakerAmountToFill
: alreadyFilledTakerAmount.plus(takerAssetFillAmount);
const expFilledMakerAmount = orderUtils.getPartialAmountFloor(
expFilledTakerAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
const expMakerFeePaid = orderUtils.getPartialAmountFloor(
expFilledTakerAmount,
signedOrder.takerAssetAmount,
signedOrder.makerFee,
);
const expTakerFeePaid = orderUtils.getPartialAmountFloor(
expFilledTakerAmount,
signedOrder.takerAssetAmount,
signedOrder.takerFee,
);
const fillResults = await this.exchangeWrapper.getFillOrderResultsAsync(signedOrder, this.takerAddress, {
takerAssetFillAmount,
});
expect(fillResults.takerAssetFilledAmount).to.be.bignumber.equal(
expFilledTakerAmount,
'takerAssetFilledAmount',
);
expect(fillResults.makerAssetFilledAmount).to.be.bignumber.equal(
expFilledMakerAmount,
'makerAssetFilledAmount',
);
expect(fillResults.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'takerFeePaid');
expect(fillResults.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'makerFeePaid');
// - Let's fill the order!
const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, {
takerAssetFillAmount,
});
const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount');
const exchangeLogs = _.filter(
txReceipt.logs,
txLog => txLog.address === this.exchangeWrapper.getExchangeAddress(),
);
expect(exchangeLogs.length).to.be.equal(1, 'logs length');
// tslint:disable-next-line:no-unnecessary-type-assertion
const log = txReceipt.logs[0] as LogWithDecodedArgs<ExchangeFillEventArgs>;
expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress');
expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress');
expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress');
expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal(
expFilledMakerAmount,
'log.args.makerAssetFilledAmount',
);
expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal(
expFilledTakerAmount,
'log.args.takerAssetFilledAmount',
);
expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid');
expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid');
expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash');
expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData');
expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData');
const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData);
expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal(
expMakerAssetBalanceOfMaker,
'makerAssetBalanceOfMaker',
);
const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
makerAddress,
makerAssetData,
);
expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal(
expMakerAssetAllowanceOfMaker,
'makerAssetAllowanceOfMaker',
);
const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData);
expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal(
expTakerAssetBalanceOfMaker,
'takerAssetBalanceOfMaker',
);
const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData);
expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker');
const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
makerAddress,
this.zrxAssetData,
);
expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal(
expZRXAssetAllowanceOfMaker,
'ZRXAssetAllowanceOfMaker',
);
const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData);
expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal(
expTakerAssetBalanceOfTaker,
'TakerAssetBalanceOfTaker',
);
const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
this.takerAddress,
takerAssetData,
);
expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal(
expTakerAssetAllowanceOfTaker,
'TakerAssetAllowanceOfTaker',
);
const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData);
expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal(
expMakerAssetBalanceOfTaker,
'MakerAssetBalanceOfTaker',
);
const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData);
expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker');
const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
this.takerAddress,
this.zrxAssetData,
);
expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal(
expZRXAssetAllowanceOfTaker,
'ZRXAssetAllowanceOfTaker',
);
const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync(
feeRecipient,
this.zrxAssetData,
);
expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal(
expZRXAssetBalanceOfFeeRecipient,
'ZRXAssetBalanceOfFeeRecipient',
);
}
private async _abiEncodeFillOrderAndAssertOutcomeAsync(
signedOrder: SignedOrder,
takerAssetFillAmount: BigNumber,
): Promise<void> {
const params = orderUtils.createFill(signedOrder, takerAssetFillAmount);
const abiDataEncodedByContract = await this.testLibsContract.publicAbiEncodeFillOrder.callAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
);
const paramsDecodedByClient = this.exchangeWrapper.abiDecodeFillOrder(abiDataEncodedByContract);
expect(paramsDecodedByClient).to.be.deep.equal(params, 'ABIEncodedFillOrderData');
}
private async _getTakerAssetFillAmountAsync(
signedOrder: SignedOrder,
takerAssetFillAmountScenario: TakerAssetFillAmountScenario,
balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher,
orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher,
): Promise<BigNumber> {
const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher);
const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync(
signedOrder,
this.takerAddress,
);
let takerAssetFillAmount;
switch (takerAssetFillAmountScenario) {
case TakerAssetFillAmountScenario.Zero:
takerAssetFillAmount = new BigNumber(0);
break;
case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount:
takerAssetFillAmount = fillableTakerAssetAmount;
break;
case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount:
takerAssetFillAmount = fillableTakerAssetAmount.plus(1);
break;
case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount:
const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData);
const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData);
const isEitherAssetERC721 =
takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721;
if (isEitherAssetERC721) {
throw new Error(
'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.',
);
}
takerAssetFillAmount = fillableTakerAssetAmount.div(2).integerValue(BigNumber.ROUND_FLOOR);
break;
default:
throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario);
}
return takerAssetFillAmount;
}
private async _modifyTraderStateAsync(
makerStateScenario: TraderStateScenario,
takerStateScenario: TraderStateScenario,
signedOrder: SignedOrder,
takerAssetFillAmount: BigNumber,
): Promise<void> {
const makerAssetFillAmount = orderUtils.getPartialAmountFloor(
takerAssetFillAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
switch (makerStateScenario.traderAssetBalance) {
case BalanceAmountScenario.Higher:
break; // Noop since this is already the default
case BalanceAmountScenario.TooLow:
if (makerAssetFillAmount.eq(0)) {
throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`);
}
const tooLowBalance = makerAssetFillAmount.minus(1);
await this.assetWrapper.setBalanceAsync(
signedOrder.makerAddress,
signedOrder.makerAssetData,
tooLowBalance,
);
break;
case BalanceAmountScenario.Exact:
const exactBalance = makerAssetFillAmount;
await this.assetWrapper.setBalanceAsync(
signedOrder.makerAddress,
signedOrder.makerAssetData,
exactBalance,
);
break;
default:
throw errorUtils.spawnSwitchErr(
'makerStateScenario.traderAssetBalance',
makerStateScenario.traderAssetBalance,
);
}
const makerFee = orderUtils.getPartialAmountFloor(
takerAssetFillAmount,
signedOrder.takerAssetAmount,
signedOrder.makerFee,
);
switch (makerStateScenario.zrxFeeBalance) {
case BalanceAmountScenario.Higher:
break; // Noop since this is already the default
case BalanceAmountScenario.TooLow:
if (makerFee.eq(0)) {
throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`);
}
const tooLowBalance = makerFee.minus(1);
await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance);
break;
case BalanceAmountScenario.Exact:
const exactBalance = makerFee;
await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance);
break;
default:
throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance);
}
switch (makerStateScenario.traderAssetAllowance) {
case AllowanceAmountScenario.Higher:
break; // Noop since this is already the default
case AllowanceAmountScenario.TooLow:
const tooLowAllowance = makerAssetFillAmount.minus(1);
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.makerAddress,
signedOrder.makerAssetData,
tooLowAllowance,
);
break;
case AllowanceAmountScenario.Exact:
const exactAllowance = makerAssetFillAmount;
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.makerAddress,
signedOrder.makerAssetData,
exactAllowance,
);
break;
case AllowanceAmountScenario.Unlimited:
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.makerAddress,
signedOrder.makerAssetData,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
);
break;
default:
throw errorUtils.spawnSwitchErr(
'makerStateScenario.traderAssetAllowance',
makerStateScenario.traderAssetAllowance,
);
}
switch (makerStateScenario.zrxFeeAllowance) {
case AllowanceAmountScenario.Higher:
break; // Noop since this is already the default
case AllowanceAmountScenario.TooLow:
const tooLowAllowance = makerFee.minus(1);
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.makerAddress,
this.zrxAssetData,
tooLowAllowance,
);
break;
case AllowanceAmountScenario.Exact:
const exactAllowance = makerFee;
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.makerAddress,
this.zrxAssetData,
exactAllowance,
);
break;
case AllowanceAmountScenario.Unlimited:
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.makerAddress,
this.zrxAssetData,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
);
break;
default:
throw errorUtils.spawnSwitchErr(
'makerStateScenario.zrxFeeAllowance',
makerStateScenario.zrxFeeAllowance,
);
}
switch (takerStateScenario.traderAssetBalance) {
case BalanceAmountScenario.Higher:
break; // Noop since this is already the default
case BalanceAmountScenario.TooLow:
if (takerAssetFillAmount.eq(0)) {
throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`);
}
const tooLowBalance = takerAssetFillAmount.minus(1);
await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance);
break;
case BalanceAmountScenario.Exact:
const exactBalance = takerAssetFillAmount;
await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance);
break;
default:
throw errorUtils.spawnSwitchErr(
'takerStateScenario.traderAssetBalance',
takerStateScenario.traderAssetBalance,
);
}
const takerFee = orderUtils.getPartialAmountFloor(
takerAssetFillAmount,
signedOrder.takerAssetAmount,
signedOrder.takerFee,
);
switch (takerStateScenario.zrxFeeBalance) {
case BalanceAmountScenario.Higher:
break; // Noop since this is already the default
case BalanceAmountScenario.TooLow:
if (takerFee.eq(0)) {
throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`);
}
const tooLowBalance = takerFee.minus(1);
await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance);
break;
case BalanceAmountScenario.Exact:
const exactBalance = takerFee;
await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance);
break;
default:
throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance);
}
switch (takerStateScenario.traderAssetAllowance) {
case AllowanceAmountScenario.Higher:
break; // Noop since this is already the default
case AllowanceAmountScenario.TooLow:
const tooLowAllowance = takerAssetFillAmount.minus(1);
await this.assetWrapper.setProxyAllowanceAsync(
this.takerAddress,
signedOrder.takerAssetData,
tooLowAllowance,
);
break;
case AllowanceAmountScenario.Exact:
const exactAllowance = takerAssetFillAmount;
await this.assetWrapper.setProxyAllowanceAsync(
this.takerAddress,
signedOrder.takerAssetData,
exactAllowance,
);
break;
case AllowanceAmountScenario.Unlimited:
await this.assetWrapper.setProxyAllowanceAsync(
this.takerAddress,
signedOrder.takerAssetData,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
);
break;
default:
throw errorUtils.spawnSwitchErr(
'takerStateScenario.traderAssetAllowance',
takerStateScenario.traderAssetAllowance,
);
}
switch (takerStateScenario.zrxFeeAllowance) {
case AllowanceAmountScenario.Higher:
break; // Noop since this is already the default
case AllowanceAmountScenario.TooLow:
const tooLowAllowance = takerFee.minus(1);
await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance);
break;
case AllowanceAmountScenario.Exact:
const exactAllowance = takerFee;
await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance);
break;
case AllowanceAmountScenario.Unlimited:
await this.assetWrapper.setProxyAllowanceAsync(
this.takerAddress,
this.zrxAssetData,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
);
break;
default:
throw errorUtils.spawnSwitchErr(
'takerStateScenario.zrxFeeAllowance',
takerStateScenario.zrxFeeAllowance,
);
}
}
} // tslint:disable:max-file-line-count

View File

@@ -0,0 +1,3 @@
export * from './exchange_wrapper';
export * from './erc20_wrapper';
export * from './erc721_wrapper';

View File

@@ -0,0 +1,564 @@
import {
chaiSetup,
ERC20BalancesByOwner,
ERC721TokenIdsByOwner,
OrderInfo,
OrderStatus,
TransferAmountsByMatchOrders as TransferAmounts,
TransferAmountsLoggedByMatchOrders as LoggedTransferAmounts,
} from '@0x/contracts-test-utils';
import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
import { AssetProxyId, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { TransactionReceiptWithDecodedLogs } from '../../../../node_modules/ethereum-types';
import { ERC20Wrapper } from './erc20_wrapper';
import { ERC721Wrapper } from './erc721_wrapper';
import { ExchangeWrapper } from './exchange_wrapper';
chaiSetup.configure();
const expect = chai.expect;
export class MatchOrderTester {
private readonly _exchangeWrapper: ExchangeWrapper;
private readonly _erc20Wrapper: ERC20Wrapper;
private readonly _erc721Wrapper: ERC721Wrapper;
private readonly _feeTokenAddress: string;
/// @dev Checks values from the logs produced by Exchange.matchOrders against the expected transfer amounts.
/// Values include the amounts transferred from the left/right makers and taker, along with
/// the fees paid on each matched order. These are also the return values of MatchOrders.
/// @param signedOrderLeft First matched order.
/// @param signedOrderRight Second matched order.
/// @param transactionReceipt Transaction receipt and logs produced by Exchange.matchOrders.
/// @param takerAddress Address of taker (account that called Exchange.matchOrders)
/// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
private static async _assertLogsAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
transactionReceipt: TransactionReceiptWithDecodedLogs,
takerAddress: string,
expectedTransferAmounts: TransferAmounts,
): Promise<void> {
// Should have two fill event logs -- one for each order.
const transactionFillLogs = _.filter(transactionReceipt.logs, ['event', 'Fill']);
expect(transactionFillLogs.length, 'Checking number of logs').to.be.equal(2);
// First log is for left fill
const leftLog = (transactionFillLogs[0] as any).args as LoggedTransferAmounts;
expect(leftLog.makerAddress, 'Checking logged maker address of left order').to.be.equal(
signedOrderLeft.makerAddress,
);
expect(leftLog.takerAddress, 'Checking logged taker address of right order').to.be.equal(takerAddress);
const amountBoughtByLeftMaker = new BigNumber(leftLog.takerAssetFilledAmount);
const amountSoldByLeftMaker = new BigNumber(leftLog.makerAssetFilledAmount);
const feePaidByLeftMaker = new BigNumber(leftLog.makerFeePaid);
const feePaidByTakerLeft = new BigNumber(leftLog.takerFeePaid);
// Second log is for right fill
const rightLog = (transactionFillLogs[1] as any).args as LoggedTransferAmounts;
expect(rightLog.makerAddress, 'Checking logged maker address of right order').to.be.equal(
signedOrderRight.makerAddress,
);
expect(rightLog.takerAddress, 'Checking loggerd taker address of right order').to.be.equal(takerAddress);
const amountBoughtByRightMaker = new BigNumber(rightLog.takerAssetFilledAmount);
const amountSoldByRightMaker = new BigNumber(rightLog.makerAssetFilledAmount);
const feePaidByRightMaker = new BigNumber(rightLog.makerFeePaid);
const feePaidByTakerRight = new BigNumber(rightLog.takerFeePaid);
// Derive amount received by taker
const amountReceivedByTaker = amountSoldByLeftMaker.minus(amountBoughtByRightMaker);
// Assert log values - left order
expect(amountBoughtByLeftMaker, 'Checking logged amount bought by left maker').to.be.bignumber.equal(
expectedTransferAmounts.amountBoughtByLeftMaker,
);
expect(amountSoldByLeftMaker, 'Checking logged amount sold by left maker').to.be.bignumber.equal(
expectedTransferAmounts.amountSoldByLeftMaker,
);
expect(feePaidByLeftMaker, 'Checking logged fee paid by left maker').to.be.bignumber.equal(
expectedTransferAmounts.feePaidByLeftMaker,
);
expect(feePaidByTakerLeft, 'Checking logged fee paid on left order by taker').to.be.bignumber.equal(
expectedTransferAmounts.feePaidByTakerLeft,
);
// Assert log values - right order
expect(amountBoughtByRightMaker, 'Checking logged amount bought by right maker').to.be.bignumber.equal(
expectedTransferAmounts.amountBoughtByRightMaker,
);
expect(amountSoldByRightMaker, 'Checking logged amount sold by right maker').to.be.bignumber.equal(
expectedTransferAmounts.amountSoldByRightMaker,
);
expect(feePaidByRightMaker, 'Checking logged fee paid by right maker').to.be.bignumber.equal(
expectedTransferAmounts.feePaidByRightMaker,
);
expect(feePaidByTakerRight, 'Checking logged fee paid on right order by taker').to.be.bignumber.equal(
expectedTransferAmounts.feePaidByTakerRight,
);
// Assert derived amount received by taker
expect(amountReceivedByTaker, 'Checking logged amount received by taker').to.be.bignumber.equal(
expectedTransferAmounts.amountReceivedByTaker,
);
}
/// @dev Asserts all expected ERC20 and ERC721 account holdings match the real holdings.
/// @param expectedERC20BalancesByOwner Expected ERC20 balances.
/// @param realERC20BalancesByOwner Real ERC20 balances.
/// @param expectedERC721TokenIdsByOwner Expected ERC721 token owners.
/// @param realERC721TokenIdsByOwner Real ERC20 token owners.
private static async _assertAllKnownBalancesAsync(
expectedERC20BalancesByOwner: ERC20BalancesByOwner,
realERC20BalancesByOwner: ERC20BalancesByOwner,
expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
realERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
): Promise<void> {
// ERC20 Balances
const areERC20BalancesEqual = _.isEqual(expectedERC20BalancesByOwner, realERC20BalancesByOwner);
expect(areERC20BalancesEqual, 'Checking all known ERC20 account balances').to.be.true();
// ERC721 Token Ids
const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues(expectedERC721TokenIdsByOwner, tokenIdsByOwner => {
_.mapValues(tokenIdsByOwner, tokenIds => {
_.sortBy(tokenIds);
});
});
const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => {
_.mapValues(tokenIdsByOwner, tokenIds => {
_.sortBy(tokenIds);
});
});
const areERC721TokenIdsEqual = _.isEqual(
sortedExpectedNewERC721TokenIdsByOwner,
sortedNewERC721TokenIdsByOwner,
);
expect(areERC721TokenIdsEqual, 'Checking all known ERC721 account balances').to.be.true();
}
/// @dev Constructs new MatchOrderTester.
/// @param exchangeWrapper Used to call to the Exchange.
/// @param erc20Wrapper Used to fetch ERC20 balances.
/// @param erc721Wrapper Used to fetch ERC721 token owners.
/// @param feeTokenAddress Address of ERC20 fee token.
constructor(
exchangeWrapper: ExchangeWrapper,
erc20Wrapper: ERC20Wrapper,
erc721Wrapper: ERC721Wrapper,
feeTokenAddress: string,
) {
this._exchangeWrapper = exchangeWrapper;
this._erc20Wrapper = erc20Wrapper;
this._erc721Wrapper = erc721Wrapper;
this._feeTokenAddress = feeTokenAddress;
}
/// @dev Matches two complementary orders and asserts results.
/// @param signedOrderLeft First matched order.
/// @param signedOrderRight Second matched order.
/// @param takerAddress Address of taker (the address who matched the two orders)
/// @param erc20BalancesByOwner Current ERC20 balances.
/// @param erc721TokenIdsByOwner Current ERC721 token owners.
/// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
/// @param initialLeftOrderFilledAmount How much left order has been filled, prior to matching orders.
/// @param initialRightOrderFilledAmount How much the right order has been filled, prior to matching orders.
/// @return New ERC20 balances & ERC721 token owners.
public async matchOrdersAndAssertEffectsAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
takerAddress: string,
erc20BalancesByOwner: ERC20BalancesByOwner,
erc721TokenIdsByOwner: ERC721TokenIdsByOwner,
expectedTransferAmounts: TransferAmounts,
initialLeftOrderFilledAmount: BigNumber = new BigNumber(0),
initialRightOrderFilledAmount: BigNumber = new BigNumber(0),
): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> {
// Assert initial order states
await this._assertInitialOrderStatesAsync(
signedOrderLeft,
signedOrderRight,
initialLeftOrderFilledAmount,
initialRightOrderFilledAmount,
);
// Match left & right orders
const transactionReceipt = await this._exchangeWrapper.matchOrdersAsync(
signedOrderLeft,
signedOrderRight,
takerAddress,
);
const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync();
const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync();
// Assert logs
await MatchOrderTester._assertLogsAsync(
signedOrderLeft,
signedOrderRight,
transactionReceipt,
takerAddress,
expectedTransferAmounts,
);
// Assert exchange state
await this._assertExchangeStateAsync(
signedOrderLeft,
signedOrderRight,
initialLeftOrderFilledAmount,
initialRightOrderFilledAmount,
expectedTransferAmounts,
);
// Assert balances of makers, taker, and fee recipients
await this._assertBalancesAsync(
signedOrderLeft,
signedOrderRight,
erc20BalancesByOwner,
erc721TokenIdsByOwner,
newERC20BalancesByOwner,
newERC721TokenIdsByOwner,
expectedTransferAmounts,
takerAddress,
);
return [newERC20BalancesByOwner, newERC721TokenIdsByOwner];
}
/// @dev Asserts initial exchange state for the left and right orders.
/// @param signedOrderLeft First matched order.
/// @param signedOrderRight Second matched order.
/// @param expectedOrderFilledAmountLeft How much left order has been filled, prior to matching orders.
/// @param expectedOrderFilledAmountRight How much the right order has been filled, prior to matching orders.
private async _assertInitialOrderStatesAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
expectedOrderFilledAmountLeft: BigNumber,
expectedOrderFilledAmountRight: BigNumber,
): Promise<void> {
// Assert left order initial state
const orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrderLeft),
);
expect(orderTakerAssetFilledAmountLeft, 'Checking inital state of left order').to.be.bignumber.equal(
expectedOrderFilledAmountLeft,
);
// Assert right order initial state
const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrderRight),
);
expect(orderTakerAssetFilledAmountRight, 'Checking inital state of right order').to.be.bignumber.equal(
expectedOrderFilledAmountRight,
);
}
/// @dev Asserts the exchange state against the expected amounts transferred by from matching orders.
/// @param signedOrderLeft First matched order.
/// @param signedOrderRight Second matched order.
/// @param initialLeftOrderFilledAmount How much left order has been filled, prior to matching orders.
/// @param initialRightOrderFilledAmount How much the right order has been filled, prior to matching orders.
/// @return TransferAmounts A struct containing the expected transfer amounts.
private async _assertExchangeStateAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
initialLeftOrderFilledAmount: BigNumber,
initialRightOrderFilledAmount: BigNumber,
expectedTransferAmounts: TransferAmounts,
): Promise<void> {
// Assert state for left order: amount bought by left maker
let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrderLeft),
);
amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(initialLeftOrderFilledAmount);
expect(amountBoughtByLeftMaker, 'Checking exchange state for left order').to.be.bignumber.equal(
expectedTransferAmounts.amountBoughtByLeftMaker,
);
// Assert state for right order: amount bought by right maker
let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrderRight),
);
amountBoughtByRightMaker = amountBoughtByRightMaker.minus(initialRightOrderFilledAmount);
expect(amountBoughtByRightMaker, 'Checking exchange state for right order').to.be.bignumber.equal(
expectedTransferAmounts.amountBoughtByRightMaker,
);
// Assert left order status
const maxAmountBoughtByLeftMaker = signedOrderLeft.takerAssetAmount.minus(initialLeftOrderFilledAmount);
const leftOrderInfo: OrderInfo = await this._exchangeWrapper.getOrderInfoAsync(signedOrderLeft);
const leftExpectedStatus = expectedTransferAmounts.amountBoughtByLeftMaker.isEqualTo(maxAmountBoughtByLeftMaker)
? OrderStatus.FullyFilled
: OrderStatus.Fillable;
expect(leftOrderInfo.orderStatus, 'Checking exchange status for left order').to.be.equal(leftExpectedStatus);
// Assert right order status
const maxAmountBoughtByRightMaker = signedOrderRight.takerAssetAmount.minus(initialRightOrderFilledAmount);
const rightOrderInfo: OrderInfo = await this._exchangeWrapper.getOrderInfoAsync(signedOrderRight);
const rightExpectedStatus = expectedTransferAmounts.amountBoughtByRightMaker.isEqualTo(
maxAmountBoughtByRightMaker,
)
? OrderStatus.FullyFilled
: OrderStatus.Fillable;
expect(rightOrderInfo.orderStatus, 'Checking exchange status for right order').to.be.equal(rightExpectedStatus);
}
/// @dev Asserts account balances after matching orders.
/// @param signedOrderLeft First matched order.
/// @param signedOrderRight Second matched order.
/// @param initialERC20BalancesByOwner ERC20 balances prior to order matching.
/// @param initialERC721TokenIdsByOwner ERC721 token owners prior to order matching.
/// @param finalERC20BalancesByOwner ERC20 balances after order matching.
/// @param finalERC721TokenIdsByOwner ERC721 token owners after order matching.
/// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
/// @param takerAddress Address of taker (account that called Exchange.matchOrders).
private async _assertBalancesAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
initialERC20BalancesByOwner: ERC20BalancesByOwner,
initialERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
finalERC20BalancesByOwner: ERC20BalancesByOwner,
finalERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
expectedTransferAmounts: TransferAmounts,
takerAddress: string,
): Promise<void> {
let expectedERC20BalancesByOwner: ERC20BalancesByOwner;
let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
[expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances(
signedOrderLeft,
signedOrderRight,
takerAddress,
initialERC20BalancesByOwner,
initialERC721TokenIdsByOwner,
expectedTransferAmounts,
);
// Assert balances of makers, taker, and fee recipients
await this._assertMakerTakerAndFeeRecipientBalancesAsync(
signedOrderLeft,
signedOrderRight,
expectedERC20BalancesByOwner,
finalERC20BalancesByOwner,
expectedERC721TokenIdsByOwner,
finalERC721TokenIdsByOwner,
takerAddress,
);
// Assert balances for all known accounts
await MatchOrderTester._assertAllKnownBalancesAsync(
expectedERC20BalancesByOwner,
finalERC20BalancesByOwner,
expectedERC721TokenIdsByOwner,
finalERC721TokenIdsByOwner,
);
}
/// @dev Calculates the expected balances of order makers, fee recipients, and the taker,
/// as a result of matching two orders.
/// @param signedOrderRight First matched order.
/// @param signedOrderRight Second matched order.
/// @param takerAddress Address of taker (the address who matched the two orders)
/// @param erc20BalancesByOwner Current ERC20 balances.
/// @param erc721TokenIdsByOwner Current ERC721 token owners.
/// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
/// @return Expected ERC20 balances & ERC721 token owners after orders have been matched.
private _calculateExpectedBalances(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
takerAddress: string,
erc20BalancesByOwner: ERC20BalancesByOwner,
erc721TokenIdsByOwner: ERC721TokenIdsByOwner,
expectedTransferAmounts: TransferAmounts,
): [ERC20BalancesByOwner, ERC721TokenIdsByOwner] {
const makerAddressLeft = signedOrderLeft.makerAddress;
const makerAddressRight = signedOrderRight.makerAddress;
const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress;
const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress;
// Operations are performed on copies of the balances
const expectedNewERC20BalancesByOwner = _.cloneDeep(erc20BalancesByOwner);
const expectedNewERC721TokenIdsByOwner = _.cloneDeep(erc721TokenIdsByOwner);
// Left Maker Asset (Right Taker Asset)
const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData);
if (makerAssetProxyIdLeft === AssetProxyId.ERC20) {
// Decode asset data
const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData);
const makerAssetAddressLeft = erc20AssetData.tokenAddress;
const takerAssetAddressRight = makerAssetAddressLeft;
// Left Maker
expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
makerAddressLeft
][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker);
// Right Maker
expectedNewERC20BalancesByOwner[makerAddressRight][
takerAssetAddressRight
] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].plus(
expectedTransferAmounts.amountBoughtByRightMaker,
);
// Taker
expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
takerAddress
][makerAssetAddressLeft].plus(expectedTransferAmounts.amountReceivedByTaker);
} else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) {
// Decode asset data
const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData);
const makerAssetAddressLeft = erc721AssetData.tokenAddress;
const makerAssetIdLeft = erc721AssetData.tokenId;
const takerAssetAddressRight = makerAssetAddressLeft;
const takerAssetIdRight = makerAssetIdLeft;
// Left Maker
_.remove(expectedNewERC721TokenIdsByOwner[makerAddressLeft][makerAssetAddressLeft], makerAssetIdLeft);
// Right Maker
expectedNewERC721TokenIdsByOwner[makerAddressRight][takerAssetAddressRight].push(takerAssetIdRight);
// Taker: Since there is only 1 asset transferred, the taker does not receive any of the left maker asset.
}
// Left Taker Asset (Right Maker Asset)
// Note: This exchange is only between the order makers: the Taker does not receive any of the left taker asset.
const takerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.takerAssetData);
if (takerAssetProxyIdLeft === AssetProxyId.ERC20) {
// Decode asset data
const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.takerAssetData);
const takerAssetAddressLeft = erc20AssetData.tokenAddress;
const makerAssetAddressRight = takerAssetAddressLeft;
// Left Maker
expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
makerAddressLeft
][takerAssetAddressLeft].plus(expectedTransferAmounts.amountBoughtByLeftMaker);
// Right Maker
expectedNewERC20BalancesByOwner[makerAddressRight][
makerAssetAddressRight
] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus(
expectedTransferAmounts.amountSoldByRightMaker,
);
} else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) {
// Decode asset data
const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData);
const makerAssetAddressRight = erc721AssetData.tokenAddress;
const makerAssetIdRight = erc721AssetData.tokenId;
const takerAssetAddressLeft = makerAssetAddressRight;
const takerAssetIdLeft = makerAssetIdRight;
// Right Maker
_.remove(expectedNewERC721TokenIdsByOwner[makerAddressRight][makerAssetAddressRight], makerAssetIdRight);
// Left Maker
expectedNewERC721TokenIdsByOwner[makerAddressLeft][takerAssetAddressLeft].push(takerAssetIdLeft);
}
// Left Maker Fees
expectedNewERC20BalancesByOwner[makerAddressLeft][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
makerAddressLeft
][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker);
// Right Maker Fees
expectedNewERC20BalancesByOwner[makerAddressRight][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
makerAddressRight
][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker);
// Taker Fees
expectedNewERC20BalancesByOwner[takerAddress][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
takerAddress
][this._feeTokenAddress].minus(
expectedTransferAmounts.feePaidByTakerLeft.plus(expectedTransferAmounts.feePaidByTakerRight),
);
// Left Fee Recipient Fees
expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][
this._feeTokenAddress
] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].plus(
expectedTransferAmounts.feePaidByLeftMaker.plus(expectedTransferAmounts.feePaidByTakerLeft),
);
// Right Fee Recipient Fees
expectedNewERC20BalancesByOwner[feeRecipientAddressRight][
this._feeTokenAddress
] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].plus(
expectedTransferAmounts.feePaidByRightMaker.plus(expectedTransferAmounts.feePaidByTakerRight),
);
return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner];
}
/// @dev Asserts ERC20 account balances and ERC721 token holdings that result from order matching.
/// Specifically checks balances of makers, taker and fee recipients.
/// @param signedOrderLeft First matched order.
/// @param signedOrderRight Second matched order.
/// @param expectedERC20BalancesByOwner Expected ERC20 balances.
/// @param realERC20BalancesByOwner Real ERC20 balances.
/// @param expectedERC721TokenIdsByOwner Expected ERC721 token owners.
/// @param realERC721TokenIdsByOwner Real ERC20 token owners.
/// @param takerAddress Address of taker (account that called Exchange.matchOrders).
private async _assertMakerTakerAndFeeRecipientBalancesAsync(
signedOrderLeft: SignedOrder,
signedOrderRight: SignedOrder,
expectedERC20BalancesByOwner: ERC20BalancesByOwner,
realERC20BalancesByOwner: ERC20BalancesByOwner,
expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
realERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
takerAddress: string,
): Promise<void> {
// Individual balance comparisons
const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData);
const makerERC20AssetDataLeft =
makerAssetProxyIdLeft === AssetProxyId.ERC20
? assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData)
: assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData);
const makerAssetAddressLeft = makerERC20AssetDataLeft.tokenAddress;
const makerAssetProxyIdRight = assetDataUtils.decodeAssetProxyId(signedOrderRight.makerAssetData);
const makerERC20AssetDataRight =
makerAssetProxyIdRight === AssetProxyId.ERC20
? assetDataUtils.decodeERC20AssetData(signedOrderRight.makerAssetData)
: assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData);
const makerAssetAddressRight = makerERC20AssetDataRight.tokenAddress;
if (makerAssetProxyIdLeft === AssetProxyId.ERC20) {
expect(
realERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft],
'Checking left maker egress ERC20 account balance',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft]);
expect(
realERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft],
'Checking right maker ingress ERC20 account balance',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft]);
expect(
realERC20BalancesByOwner[takerAddress][makerAssetAddressLeft],
'Checking taker ingress ERC20 account balance',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[takerAddress][makerAssetAddressLeft]);
} else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) {
expect(
realERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft].sort(),
'Checking left maker egress ERC721 account holdings',
).to.be.deep.equal(
expectedERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft].sort(),
);
expect(
realERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft].sort(),
'Checking right maker ERC721 account holdings',
).to.be.deep.equal(
expectedERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft].sort(),
);
expect(
realERC721TokenIdsByOwner[takerAddress][makerAssetAddressLeft].sort(),
'Checking taker ingress ERC721 account holdings',
).to.be.deep.equal(expectedERC721TokenIdsByOwner[takerAddress][makerAssetAddressLeft].sort());
} else {
throw new Error(`Unhandled Asset Proxy ID: ${makerAssetProxyIdLeft}`);
}
if (makerAssetProxyIdRight === AssetProxyId.ERC20) {
expect(
realERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight],
'Checking left maker ingress ERC20 account balance',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight]);
expect(
realERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
'Checking right maker egress ERC20 account balance',
).to.be.bignumber.equal(
expectedERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
);
} else if (makerAssetProxyIdRight === AssetProxyId.ERC721) {
expect(
realERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight].sort(),
'Checking left maker ingress ERC721 account holdings',
).to.be.deep.equal(
expectedERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight].sort(),
);
expect(
realERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
'Checking right maker agress ERC721 account holdings',
).to.be.deep.equal(expectedERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressRight]);
} else {
throw new Error(`Unhandled Asset Proxy ID: ${makerAssetProxyIdRight}`);
}
// Paid fees
expect(
realERC20BalancesByOwner[signedOrderLeft.makerAddress][this._feeTokenAddress],
'Checking left maker egress ERC20 account fees',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][this._feeTokenAddress]);
expect(
realERC20BalancesByOwner[signedOrderRight.makerAddress][this._feeTokenAddress],
'Checking right maker egress ERC20 account fees',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderRight.makerAddress][this._feeTokenAddress]);
expect(
realERC20BalancesByOwner[takerAddress][this._feeTokenAddress],
'Checking taker egress ERC20 account fees',
).to.be.bignumber.equal(expectedERC20BalancesByOwner[takerAddress][this._feeTokenAddress]);
// Received fees
expect(
realERC20BalancesByOwner[signedOrderLeft.feeRecipientAddress][this._feeTokenAddress],
'Checking left fee recipient ingress ERC20 account fees',
).to.be.bignumber.equal(
expectedERC20BalancesByOwner[signedOrderLeft.feeRecipientAddress][this._feeTokenAddress],
);
expect(
realERC20BalancesByOwner[signedOrderRight.feeRecipientAddress][this._feeTokenAddress],
'Checking right fee receipient ingress ERC20 account fees',
).to.be.bignumber.equal(
expectedERC20BalancesByOwner[signedOrderRight.feeRecipientAddress][this._feeTokenAddress],
);
}
} // tslint:disable-line:max-file-line-count

View File

@@ -0,0 +1,294 @@
import {
AssetDataScenario,
constants,
ERC721TokenIdsByOwner,
ExpirationTimeSecondsScenario,
FeeRecipientAddressScenario,
OrderAssetAmountScenario,
OrderScenario,
TakerScenario,
} from '@0x/contracts-test-utils';
import { DummyERC721TokenContract } from '@0x/contracts-tokens';
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { Order } from '@0x/types';
import { BigNumber, errorUtils } from '@0x/utils';
const TEN_UNITS_EIGHTEEN_DECIMALS = new BigNumber(10_000_000_000_000_000_000);
const FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(5_000_000_000_000_000_000);
const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100_000_000_000_000_000);
const POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(50_000_000_000_000_000);
const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1_000_000);
const FIVE_UNITS_FIVE_DECIMALS = new BigNumber(500_000);
const TEN_UNITS_ZERO_DECIMALS = new BigNumber(10);
const ONE_THOUSAND_UNITS_ZERO_DECIMALS = new BigNumber(1000);
const ONE_NFT_UNIT = new BigNumber(1);
export class OrderFactoryFromScenario {
private readonly _userAddresses: string[];
private readonly _zrxAddress: string;
private readonly _nonZrxERC20EighteenDecimalTokenAddresses: string[];
private readonly _erc20FiveDecimalTokenAddresses: string[];
private readonly _erc20ZeroDecimalTokenAddresses: string[];
private readonly _erc721Token: DummyERC721TokenContract;
private readonly _erc721Balances: ERC721TokenIdsByOwner;
private readonly _exchangeAddress: string;
constructor(
userAddresses: string[],
zrxAddress: string,
nonZrxERC20EighteenDecimalTokenAddresses: string[],
erc20FiveDecimalTokenAddresses: string[],
erc20ZeroDecimalTokenAddresses: string[],
erc721Token: DummyERC721TokenContract,
erc721Balances: ERC721TokenIdsByOwner,
exchangeAddress: string,
) {
this._userAddresses = userAddresses;
this._zrxAddress = zrxAddress;
this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses;
this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses;
this._erc20ZeroDecimalTokenAddresses = erc20ZeroDecimalTokenAddresses;
this._erc721Token = erc721Token;
this._erc721Balances = erc721Balances;
this._exchangeAddress = exchangeAddress;
}
public generateOrder(orderScenario: OrderScenario): Order {
const makerAddress = this._userAddresses[1];
let takerAddress = this._userAddresses[2];
const erc721MakerAssetIds = this._erc721Balances[makerAddress][this._erc721Token.address];
const erc721TakerAssetIds = this._erc721Balances[takerAddress][this._erc721Token.address];
let feeRecipientAddress;
let makerAssetAmount;
let takerAssetAmount;
let makerFee;
let takerFee;
let expirationTimeSeconds;
let makerAssetData;
let takerAssetData;
switch (orderScenario.feeRecipientScenario) {
case FeeRecipientAddressScenario.BurnAddress:
feeRecipientAddress = constants.NULL_ADDRESS;
break;
case FeeRecipientAddressScenario.EthUserAddress:
feeRecipientAddress = this._userAddresses[4];
break;
default:
throw errorUtils.spawnSwitchErr('FeeRecipientAddressScenario', orderScenario.feeRecipientScenario);
}
switch (orderScenario.makerAssetDataScenario) {
case AssetDataScenario.ZRXFeeToken:
makerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress);
break;
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
makerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[0]);
break;
case AssetDataScenario.ERC20FiveDecimals:
makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[0]);
break;
case AssetDataScenario.ERC721:
makerAssetData = assetDataUtils.encodeERC721AssetData(
this._erc721Token.address,
erc721MakerAssetIds[0],
);
break;
case AssetDataScenario.ERC20ZeroDecimals:
makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[0]);
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
switch (orderScenario.takerAssetDataScenario) {
case AssetDataScenario.ZRXFeeToken:
takerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress);
break;
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
takerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[1]);
break;
case AssetDataScenario.ERC20FiveDecimals:
takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[1]);
break;
case AssetDataScenario.ERC721:
takerAssetData = assetDataUtils.encodeERC721AssetData(
this._erc721Token.address,
erc721TakerAssetIds[0],
);
break;
case AssetDataScenario.ERC20ZeroDecimals:
takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[1]);
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
}
switch (orderScenario.makerAssetAmountScenario) {
case OrderAssetAmountScenario.Large:
switch (orderScenario.makerAssetDataScenario) {
case AssetDataScenario.ZRXFeeToken:
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
makerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS;
break;
case AssetDataScenario.ERC20FiveDecimals:
makerAssetAmount = TEN_UNITS_FIVE_DECIMALS;
break;
case AssetDataScenario.ERC721:
makerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
makerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
break;
case OrderAssetAmountScenario.Small:
switch (orderScenario.makerAssetDataScenario) {
case AssetDataScenario.ZRXFeeToken:
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
makerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS;
break;
case AssetDataScenario.ERC20FiveDecimals:
makerAssetAmount = FIVE_UNITS_FIVE_DECIMALS;
break;
case AssetDataScenario.ERC721:
makerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
makerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
break;
case OrderAssetAmountScenario.Zero:
makerAssetAmount = new BigNumber(0);
break;
default:
throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerAssetAmountScenario);
}
switch (orderScenario.takerAssetAmountScenario) {
case OrderAssetAmountScenario.Large:
switch (orderScenario.takerAssetDataScenario) {
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
case AssetDataScenario.ZRXFeeToken:
takerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS;
break;
case AssetDataScenario.ERC20FiveDecimals:
takerAssetAmount = TEN_UNITS_FIVE_DECIMALS;
break;
case AssetDataScenario.ERC721:
takerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
takerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
}
break;
case OrderAssetAmountScenario.Small:
switch (orderScenario.takerAssetDataScenario) {
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
case AssetDataScenario.ZRXFeeToken:
takerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS;
break;
case AssetDataScenario.ERC20FiveDecimals:
takerAssetAmount = FIVE_UNITS_FIVE_DECIMALS;
break;
case AssetDataScenario.ERC721:
takerAssetAmount = ONE_NFT_UNIT;
break;
case AssetDataScenario.ERC20ZeroDecimals:
takerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
}
break;
case OrderAssetAmountScenario.Zero:
takerAssetAmount = new BigNumber(0);
break;
default:
throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerAssetAmountScenario);
}
switch (orderScenario.makerFeeScenario) {
case OrderAssetAmountScenario.Large:
makerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS;
break;
case OrderAssetAmountScenario.Small:
makerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS;
break;
case OrderAssetAmountScenario.Zero:
makerFee = new BigNumber(0);
break;
default:
throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerFeeScenario);
}
switch (orderScenario.takerFeeScenario) {
case OrderAssetAmountScenario.Large:
takerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS;
break;
case OrderAssetAmountScenario.Small:
takerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS;
break;
case OrderAssetAmountScenario.Zero:
takerFee = new BigNumber(0);
break;
default:
throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerFeeScenario);
}
switch (orderScenario.expirationTimeSecondsScenario) {
case ExpirationTimeSecondsScenario.InFuture:
expirationTimeSeconds = new BigNumber(2524604400); // Close to infinite
break;
case ExpirationTimeSecondsScenario.InPast:
expirationTimeSeconds = new BigNumber(0); // Jan 1, 1970
break;
default:
throw errorUtils.spawnSwitchErr(
'ExpirationTimeSecondsScenario',
orderScenario.expirationTimeSecondsScenario,
);
}
switch (orderScenario.takerScenario) {
case TakerScenario.CorrectlySpecified:
break; // noop since takerAddress is already specified
case TakerScenario.IncorrectlySpecified:
const notTaker = this._userAddresses[3];
takerAddress = notTaker;
break;
case TakerScenario.Unspecified:
takerAddress = constants.NULL_ADDRESS;
break;
default:
throw errorUtils.spawnSwitchErr('TakerScenario', orderScenario.takerScenario);
}
const order = {
senderAddress: constants.NULL_ADDRESS,
makerAddress,
takerAddress,
makerFee,
takerFee,
makerAssetAmount,
takerAssetAmount,
makerAssetData,
takerAssetData,
salt: generatePseudoRandomSalt(),
exchangeAddress: this._exchangeAddress,
feeRecipientAddress,
expirationTimeSeconds,
};
return order;
}
}

View File

@@ -0,0 +1,19 @@
import { AbstractBalanceAndProxyAllowanceFetcher } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { AssetWrapper } from './asset_wrapper';
export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
private readonly _assetWrapper: AssetWrapper;
constructor(assetWrapper: AssetWrapper) {
this._assetWrapper = assetWrapper;
}
public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData);
return balance;
}
public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData);
return proxyAllowance;
}
}

View File

@@ -0,0 +1,31 @@
import { AbstractOrderFilledCancelledFetcher, orderHashUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { ExchangeWrapper } from './exchange_wrapper';
export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher {
private readonly _exchangeWrapper: ExchangeWrapper;
private readonly _zrxAssetData: string;
constructor(exchange: ExchangeWrapper, zrxAssetData: string) {
this._exchangeWrapper = exchange;
this._zrxAssetData = zrxAssetData;
}
public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash));
return filledTakerAmount;
}
public async isOrderCancelledAsync(signedOrder: SignedOrder): Promise<boolean> {
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash);
const orderEpoch = await this._exchangeWrapper.getOrderEpochAsync(
signedOrder.makerAddress,
signedOrder.senderAddress,
);
const isCancelledByOrderEpoch = orderEpoch > signedOrder.salt;
return isCancelled || isCancelledByOrderEpoch;
}
public getZRXAssetData(): string {
return this._zrxAssetData;
}
}

View File

@@ -0,0 +1,8 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
export interface AbiDecodedFillOrderData {
order: SignedOrder;
takerAssetFillAmount: BigNumber;
signature: string;
}