Merge branch 'v2-prototype' of https://github.com/0xProject/0x-monorepo into feature/website/portal-onboarding-polish

This commit is contained in:
fragosti
2018-06-28 17:20:05 -07:00
49 changed files with 2607 additions and 1021 deletions

View File

@@ -0,0 +1,3 @@
export abstract class AbstractAssetWrapper {
public abstract getProxyId(): string;
}

View File

@@ -56,4 +56,20 @@ contract DummyERC721Token is
);
_mint(to, tokenId);
}
/**
* @dev Function to burn a token
* @dev Reverts if the given token ID doesn't exist
* @param tokenId uint256 ID of the token to be minted by the msg.sender
*/
function burn(address owner, uint256 tokenId)
public
onlyOwner
{
require(
exists(tokenId),
"Token with tokenId does not exist."
);
_burn(owner, tokenId);
}
}

View File

@@ -0,0 +1,217 @@
import { assetProxyUtils } from '@0xproject/order-utils';
import { AssetProxyId } from '@0xproject/types';
import { BigNumber, errorUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import { AbstractAssetWrapper } from '../abstract/abstract_asset_wrapper';
import { constants } from './constants';
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 _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 = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.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 = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.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)) {
// Burn token
await erc721Wrapper.burnAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress);
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 = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.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 = assetProxyUtils.decodeAssetDataId(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 = assetProxyUtils.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,801 @@
import {
assetProxyUtils,
BalanceAndProxyAllowanceLazyStore,
ExchangeTransferSimulator,
orderHashUtils,
OrderStateUtils,
OrderValidationUtils,
} from '@0xproject/order-utils';
import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0xproject/types';
import { BigNumber, errorUtils, logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import 'make-promises-safe';
import { ExchangeContract, FillContractEventArgs } from '../generated_contract_wrappers/exchange';
import { artifacts } from '../utils/artifacts';
import { expectRevertReasonOrAlwaysFailingTransactionAsync } from '../utils/assertions';
import { AssetWrapper } from '../utils/asset_wrapper';
import { chaiSetup } from '../utils/chai_setup';
import { constants } from '../utils/constants';
import { ERC20Wrapper } from '../utils/erc20_wrapper';
import { ERC721Wrapper } from '../utils/erc721_wrapper';
import { ExchangeWrapper } from '../utils/exchange_wrapper';
import { OrderFactoryFromScenario } from '../utils/order_factory_from_scenario';
import { orderUtils } from '../utils/order_utils';
import { signingUtils } from '../utils/signing_utils';
import { SimpleAssetBalanceAndProxyAllowanceFetcher } from '../utils/simple_asset_balance_and_proxy_allowance_fetcher';
import { SimpleOrderFilledCancelledFetcher } from '../utils/simple_order_filled_cancelled_fetcher';
import {
AllowanceAmountScenario,
AssetDataScenario,
BalanceAmountScenario,
ExpirationTimeSecondsScenario,
FeeRecipientAddressScenario,
FillScenario,
OrderAssetAmountScenario,
TakerAssetFillAmountScenario,
TakerScenario,
TraderStateScenario,
} from '../utils/types';
chaiSetup.configure();
const expect = chai.expect;
/**
* Instantiates a new instance of CoreCombinatorialUtils. Since this method has some
* required async setup, a factory method is required.
* @param web3Wrapper Web3Wrapper instance
* @param txDefaults Default Ethereum tx options
* @return CoreCombinatorialUtils instance
*/
export async function coreCombinatorialUtilsFactoryAsync(
web3Wrapper: Web3Wrapper,
txDefaults: Partial<TxData>,
): Promise<CoreCombinatorialUtils> {
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 = assetProxyUtils.encodeERC20AssetData(zrxToken.address);
const erc20FiveDecimalTokenCount = 2;
const fiveDecimals = new BigNumber(18);
const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
erc20FiveDecimalTokenCount,
fiveDecimals,
);
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(AssetProxyId.ERC20, erc20Proxy.address, ownerAddress);
await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC721, 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],
erc721Token,
erc721Balances,
exchangeContract.address,
);
const coreCombinatorialUtils = new CoreCombinatorialUtils(
orderFactory,
ownerAddress,
makerAddress,
makerPrivateKey,
takerAddress,
zrxAssetData,
exchangeWrapper,
assetWrapper,
);
return coreCombinatorialUtils;
}
export class CoreCombinatorialUtils {
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 static generateFillOrderCombinations(): FillScenario[] {
const takerScenarios = [TakerScenario.Unspecified];
const feeRecipientScenarios = [FeeRecipientAddressScenario.EthUserAddress];
const makerAssetAmountScenario = [OrderAssetAmountScenario.Large];
const takerAssetAmountScenario = [OrderAssetAmountScenario.Large];
const makerFeeScenario = [OrderAssetAmountScenario.Large];
const takerFeeScenario = [OrderAssetAmountScenario.Large];
const expirationTimeSecondsScenario = [ExpirationTimeSecondsScenario.InFuture];
const makerAssetDataScenario = [
AssetDataScenario.ERC20FiveDecimals,
AssetDataScenario.ERC20NonZRXEighteenDecimals,
AssetDataScenario.ERC721,
AssetDataScenario.ZRXFeeToken,
];
const takerAssetDataScenario = [
AssetDataScenario.ERC20FiveDecimals,
AssetDataScenario.ERC20NonZRXEighteenDecimals,
AssetDataScenario.ERC721,
AssetDataScenario.ZRXFeeToken,
];
const takerAssetFillAmountScenario = [TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount];
const fillScenarioArrays = CoreCombinatorialUtils._getAllCombinations([
takerScenarios,
feeRecipientScenarios,
makerAssetAmountScenario,
takerAssetAmountScenario,
makerFeeScenario,
takerFeeScenario,
expirationTimeSecondsScenario,
makerAssetDataScenario,
takerAssetDataScenario,
takerAssetFillAmountScenario,
]);
const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => {
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: BalanceAmountScenario.Higher,
traderAssetAllowance: AllowanceAmountScenario.Higher,
zrxFeeBalance: BalanceAmountScenario.Higher,
zrxFeeAllowance: AllowanceAmountScenario.Higher,
},
takerStateScenario: {
traderAssetBalance: BalanceAmountScenario.Higher,
traderAssetAllowance: AllowanceAmountScenario.Higher,
zrxFeeBalance: BalanceAmountScenario.Higher,
zrxFeeAllowance: AllowanceAmountScenario.Higher,
},
};
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 = CoreCombinatorialUtils._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,
) {
this.orderFactory = orderFactory;
this.ownerAddress = ownerAddress;
this.makerAddress = makerAddress;
this.makerPrivateKey = makerPrivateKey;
this.takerAddress = takerAddress;
this.zrxAssetData = zrxAssetData;
this.exchangeWrapper = exchangeWrapper;
this.assetWrapper = assetWrapper;
}
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);
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,
);
}
private async _fillOrderAndAssertOutcomeAsync(
signedOrder: SignedOrder,
takerAssetFillAmount: BigNumber,
lazyStore: BalanceAndProxyAllowanceLazyStore,
fillRevertReasonIfExists: RevertReason | undefined,
): Promise<void> {
if (!_.isUndefined(fillRevertReasonIfExists)) {
return expectRevertReasonOrAlwaysFailingTransactionAsync(
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.add(takerAssetFillAmount);
const expFilledMakerAmount = orderUtils.getPartialAmount(
expFilledTakerAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
// - 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');
expect(txReceipt.logs.length).to.be.equal(1, 'logs length');
// tslint:disable-next-line:no-unnecessary-type-assertion
const log = txReceipt.logs[0] as LogWithDecodedArgs<FillContractEventArgs>;
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',
);
const expMakerFeePaid = orderUtils.getPartialAmount(
expFilledTakerAmount,
signedOrder.takerAssetAmount,
signedOrder.makerFee,
);
expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid');
const expTakerFeePaid = orderUtils.getPartialAmount(
expFilledTakerAmount,
signedOrder.takerAssetAmount,
signedOrder.takerFee,
);
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 _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.add(1);
break;
case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount:
const takerAssetProxyId = assetProxyUtils.decodeAssetDataId(signedOrder.takerAssetData);
const makerAssetProxyId = assetProxyUtils.decodeAssetDataId(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).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.getPartialAmount(
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.getPartialAmount(
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.getPartialAmount(
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(
signedOrder.takerAddress,
this.zrxAssetData,
tooLowAllowance,
);
break;
case AllowanceAmountScenario.Exact:
const exactAllowance = takerFee;
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.takerAddress,
this.zrxAssetData,
exactAllowance,
);
break;
case AllowanceAmountScenario.Unlimited:
await this.assetWrapper.setProxyAllowanceAsync(
signedOrder.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

@@ -1,3 +1,4 @@
import { assetProxyUtils } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import { Provider } from 'ethereum-types';
@@ -18,6 +19,7 @@ export class ERC20Wrapper {
private _provider: Provider;
private _dummyTokenContracts: DummyERC20TokenContract[];
private _proxyContract?: ERC20ProxyContract;
private _proxyIdIfExists?: string;
constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
this._dummyTokenContracts = [];
this._web3Wrapper = new Web3Wrapper(provider);
@@ -25,8 +27,11 @@ export class ERC20Wrapper {
this._tokenOwnerAddresses = tokenOwnerAddresses;
this._contractOwnerAddress = contractOwnerAddress;
}
public async deployDummyTokensAsync(): Promise<DummyERC20TokenContract[]> {
for (let i = 0; i < constants.NUM_DUMMY_ERC20_TO_DEPLOY; i++) {
public async deployDummyTokensAsync(
numberToDeploy: number,
decimals: BigNumber,
): Promise<DummyERC20TokenContract[]> {
for (let i = 0; i < numberToDeploy; i++) {
this._dummyTokenContracts.push(
await DummyERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.DummyERC20Token,
@@ -34,7 +39,7 @@ export class ERC20Wrapper {
txDefaults,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
decimals,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
),
);
@@ -47,8 +52,13 @@ export class ERC20Wrapper {
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();
@@ -73,6 +83,36 @@ export class ERC20Wrapper {
}
}
}
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 = {};
@@ -105,6 +145,15 @@ export class ERC20Wrapper {
const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
return tokenAddresses;
}
private _getTokenContractFromAssetData(assetData: string): DummyERC20TokenContract {
const erc20ProxyData = assetProxyUtils.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"');

View File

@@ -19,6 +19,7 @@ export class ERC721Wrapper {
private _provider: Provider;
private _dummyTokenContracts: DummyERC721TokenContract[];
private _proxyContract?: ERC721ProxyContract;
private _proxyIdIfExists?: string;
private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {};
constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
this._web3Wrapper = new Web3Wrapper(provider);
@@ -47,8 +48,13 @@ export class ERC721Wrapper {
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();
@@ -57,12 +63,7 @@ export class ERC721Wrapper {
for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
for (let i = 0; i < constants.NUM_ERC721_TOKENS_TO_MINT; i++) {
const tokenId = generatePseudoRandomSalt();
await this._web3Wrapper.awaitTransactionSuccessAsync(
await dummyTokenContract.mint.sendTransactionAsync(tokenOwnerAddress, tokenId, {
from: this._contractOwnerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress);
if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) {
this._initialTokenIdsByOwner[tokenOwnerAddress] = {
[dummyTokenContract.address]: [],
@@ -72,19 +73,100 @@ export class ERC721Wrapper {
this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address] = [];
}
this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address].push(tokenId);
await this.approveProxyAsync(dummyTokenContract.address, tokenId);
}
const shouldApprove = true;
await this._web3Wrapper.awaitTransactionSuccessAsync(
await dummyTokenContract.setApprovalForAll.sendTransactionAsync(
(this._proxyContract as ERC721ProxyContract).address,
shouldApprove,
{ from: tokenOwnerAddress },
),
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
}
}
public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
const doesExist = await tokenContract.exists.callAsync(tokenId);
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();
@@ -127,6 +209,13 @@ export class ERC721Wrapper {
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"');

View File

@@ -33,8 +33,8 @@ export class ExchangeWrapper {
params.signature,
{ from },
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return txReceipt;
}
public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createCancel(signedOrder);
@@ -227,6 +227,10 @@ export class ExchangeWrapper {
const filledAmount = new BigNumber(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 getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> {
const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo;
return orderInfo;

View File

@@ -0,0 +1,277 @@
import { assetProxyUtils, generatePseudoRandomSalt } from '@0xproject/order-utils';
import { Order } from '@0xproject/types';
import { BigNumber, errorUtils } from '@0xproject/utils';
import { DummyERC721TokenContract } from '../generated_contract_wrappers/dummy_e_r_c721_token';
import { constants } from './constants';
import {
AssetDataScenario,
ERC721TokenIdsByOwner,
ExpirationTimeSecondsScenario,
FeeRecipientAddressScenario,
OrderAssetAmountScenario,
OrderScenario,
TakerScenario,
} from './types';
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 ONE_NFT_UNIT = new BigNumber(1);
export class OrderFactoryFromScenario {
private _userAddresses: string[];
private _zrxAddress: string;
private _nonZrxERC20EighteenDecimalTokenAddresses: string[];
private _erc20FiveDecimalTokenAddresses: string[];
private _erc721Token: DummyERC721TokenContract;
private _erc721Balances: ERC721TokenIdsByOwner;
private _exchangeAddress: string;
constructor(
userAddresses: string[],
zrxAddress: string,
nonZrxERC20EighteenDecimalTokenAddresses: string[],
erc20FiveDecimalTokenAddresses: string[],
erc721Token: DummyERC721TokenContract,
erc721Balances: ERC721TokenIdsByOwner,
exchangeAddress: string,
) {
this._userAddresses = userAddresses;
this._zrxAddress = zrxAddress;
this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses;
this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses;
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 = assetProxyUtils.encodeERC20AssetData(this._zrxAddress);
break;
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
makerAssetData = assetProxyUtils.encodeERC20AssetData(
this._nonZrxERC20EighteenDecimalTokenAddresses[0],
);
break;
case AssetDataScenario.ERC20FiveDecimals:
makerAssetData = assetProxyUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[0]);
break;
case AssetDataScenario.ERC721:
makerAssetData = assetProxyUtils.encodeERC721AssetData(
this._erc721Token.address,
erc721MakerAssetIds[0],
);
break;
default:
throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
}
switch (orderScenario.takerAssetDataScenario) {
case AssetDataScenario.ZRXFeeToken:
takerAssetData = assetProxyUtils.encodeERC20AssetData(this._zrxAddress);
break;
case AssetDataScenario.ERC20NonZRXEighteenDecimals:
takerAssetData = assetProxyUtils.encodeERC20AssetData(
this._nonZrxERC20EighteenDecimalTokenAddresses[1],
);
break;
case AssetDataScenario.ERC20FiveDecimals:
takerAssetData = assetProxyUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[1]);
break;
case AssetDataScenario.ERC721:
takerAssetData = assetProxyUtils.encodeERC721AssetData(
this._erc721Token.address,
erc721TakerAssetIds[0],
);
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;
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;
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;
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;
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

@@ -5,6 +5,13 @@ import { constants } from './constants';
import { CancelOrder, MatchOrder } from './types';
export const orderUtils = {
getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
const partialAmount = numerator
.mul(target)
.div(denominator)
.floor();
return partialAmount;
},
createFill: (signedOrder: SignedOrder, takerAssetFillAmount?: BigNumber) => {
const fill = {
order: orderUtils.getOrderWithoutExchangeAddress(signedOrder),

View File

@@ -0,0 +1,19 @@
import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import { AssetWrapper } from './asset_wrapper';
export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
private _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,24 @@
import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import { ExchangeWrapper } from './exchange_wrapper';
export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher {
private _exchangeWrapper: ExchangeWrapper;
private _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(orderHash: string): Promise<boolean> {
const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash);
return isCancelled;
}
public getZRXAssetData(): string {
return this._zrxAssetData;
}
}

View File

@@ -150,3 +150,80 @@ export interface MatchOrder {
leftSignature: string;
rightSignature: string;
}
// Combinatorial testing types
export enum FeeRecipientAddressScenario {
BurnAddress = 'BURN_ADDRESS',
EthUserAddress = 'ETH_USER_ADDRESS',
}
export enum OrderAssetAmountScenario {
Zero = 'ZERO',
Large = 'LARGE',
Small = 'SMALL',
}
export enum TakerScenario {
CorrectlySpecified = 'CORRECTLY_SPECIFIED',
IncorrectlySpecified = 'INCORRECTLY_SPECIFIED',
Unspecified = 'UNSPECIFIED',
}
export enum ExpirationTimeSecondsScenario {
InPast = 'IN_PAST',
InFuture = 'IN_FUTURE',
}
export enum AssetDataScenario {
ERC721 = 'ERC721',
ZRXFeeToken = 'ZRX_FEE_TOKEN',
ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS',
ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS',
}
export enum TakerAssetFillAmountScenario {
Zero = 'ZERO',
GreaterThanRemainingFillableTakerAssetAmount = 'GREATER_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT',
LessThanRemainingFillableTakerAssetAmount = 'LESS_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT',
ExactlyRemainingFillableTakerAssetAmount = 'EXACTLY_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT',
}
export interface OrderScenario {
takerScenario: TakerScenario;
feeRecipientScenario: FeeRecipientAddressScenario;
makerAssetAmountScenario: OrderAssetAmountScenario;
takerAssetAmountScenario: OrderAssetAmountScenario;
makerFeeScenario: OrderAssetAmountScenario;
takerFeeScenario: OrderAssetAmountScenario;
expirationTimeSecondsScenario: ExpirationTimeSecondsScenario;
makerAssetDataScenario: AssetDataScenario;
takerAssetDataScenario: AssetDataScenario;
}
export enum BalanceAmountScenario {
Exact = 'EXACT',
TooLow = 'TOO_LOW',
Higher = 'HIGHER',
}
export enum AllowanceAmountScenario {
Exact = 'EXACT',
TooLow = 'TOO_LOW',
Higher = 'HIGHER',
Unlimited = 'UNLIMITED',
}
export interface TraderStateScenario {
traderAssetBalance: BalanceAmountScenario;
traderAssetAllowance: AllowanceAmountScenario;
zrxFeeBalance: BalanceAmountScenario;
zrxFeeAllowance: AllowanceAmountScenario;
}
export interface FillScenario {
orderScenario: OrderScenario;
takerAssetFillAmountScenario: TakerAssetFillAmountScenario;
makerStateScenario: TraderStateScenario;
takerStateScenario: TraderStateScenario;
}

View File

@@ -5,6 +5,7 @@ import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types';
import ethUtil = require('ethereumjs-util');
import * as _ from 'lodash';
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
import {
@@ -53,12 +54,17 @@ describe('Asset Transfer Proxies', () => {
});
before(async () => {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = accounts);
const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = _.slice(
accounts,
0,
5,
));
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
[zrxToken] = await erc20Wrapper.deployDummyTokensAsync();
const numDummyErc20ToDeploy = 1;
[zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
await web3Wrapper.awaitTransactionSuccessAsync(

View File

@@ -6,16 +6,13 @@ import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types';
import ethUtil = require('ethereumjs-util');
import * as _ from 'lodash';
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
import { DummyERC721TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c721_token';
import { ERC20ProxyContract } from '../../src/generated_contract_wrappers/e_r_c20_proxy';
import { ERC721ProxyContract } from '../../src/generated_contract_wrappers/e_r_c721_proxy';
import {
CancelContractEventArgs,
ExchangeContract,
FillContractEventArgs,
} from '../../src/generated_contract_wrappers/exchange';
import { CancelContractEventArgs, ExchangeContract } from '../../src/generated_contract_wrappers/exchange';
import { artifacts } from '../../src/utils/artifacts';
import { expectRevertReasonOrAlwaysFailingTransactionAsync } from '../../src/utils/assertions';
import { chaiSetup } from '../../src/utils/chai_setup';
@@ -66,12 +63,16 @@ describe('Exchange core', () => {
});
before(async () => {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts);
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4));
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync();
const numDummyErc20ToDeploy = 3;
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
numDummyErc20ToDeploy,
constants.DUMMY_TOKEN_DECIMALS,
);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
@@ -130,296 +131,6 @@ describe('Exchange core', () => {
erc20Balances = await erc20Wrapper.getBalancesAsync();
signedOrder = orderFactory.newSignedOrder();
});
it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
});
const takerAssetFilledAmountBefore = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(takerAssetFilledAmountBefore).to.be.bignumber.equal(0);
const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
const makerAmountBoughtAfter = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(makerAmountBoughtAfter).to.be.bignumber.equal(takerAssetFillAmount);
const newBalances = await erc20Wrapper.getBalancesAsync();
const makerAssetFilledAmount = takerAssetFillAmount
.times(signedOrder.makerAssetAmount)
.dividedToIntegerBy(signedOrder.takerAssetAmount);
const makerFeePaid = signedOrder.makerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const takerFeePaid = signedOrder.takerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
);
expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
);
expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
);
});
it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
});
const takerAssetFilledAmountBefore = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(takerAssetFilledAmountBefore).to.be.bignumber.equal(0);
const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
const makerAmountBoughtAfter = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(makerAmountBoughtAfter).to.be.bignumber.equal(takerAssetFillAmount);
const newBalances = await erc20Wrapper.getBalancesAsync();
const makerAssetFilledAmount = takerAssetFillAmount
.times(signedOrder.makerAssetAmount)
.dividedToIntegerBy(signedOrder.takerAssetAmount);
const makerFeePaid = signedOrder.makerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const takerFeePaid = signedOrder.takerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
);
expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
);
expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
);
});
it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
});
const takerAssetFilledAmountBefore = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(takerAssetFilledAmountBefore).to.be.bignumber.equal(0);
const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
const makerAmountBoughtAfter = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(makerAmountBoughtAfter).to.be.bignumber.equal(takerAssetFillAmount);
const newBalances = await erc20Wrapper.getBalancesAsync();
const makerAssetFilledAmount = takerAssetFillAmount
.times(signedOrder.makerAssetAmount)
.dividedToIntegerBy(signedOrder.takerAssetAmount);
const makerFeePaid = signedOrder.makerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const takerFeePaid = signedOrder.takerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
);
expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
);
expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
);
});
it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
signedOrder = orderFactory.newSignedOrder({
takerAddress,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
});
const takerAssetFilledAmountBefore = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
expect(takerAssetFilledAmountBefore).to.be.bignumber.equal(0);
const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
const makerAmountBoughtAfter = await exchangeWrapper.getTakerAssetFilledAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
const expectedMakerAmountBoughtAfter = takerAssetFillAmount.add(takerAssetFilledAmountBefore);
expect(makerAmountBoughtAfter).to.be.bignumber.equal(expectedMakerAmountBoughtAfter);
const newBalances = await erc20Wrapper.getBalancesAsync();
const makerAssetFilledAmount = takerAssetFillAmount
.times(signedOrder.makerAssetAmount)
.dividedToIntegerBy(signedOrder.takerAssetAmount);
const makerFeePaid = signedOrder.makerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const takerFeePaid = signedOrder.takerFee
.times(makerAssetFilledAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
);
expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
);
expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
);
});
it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => {
const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
takerAssetFillAmount: signedOrder.takerAssetAmount,
});
const log = res.logs[0] as LogWithDecodedArgs<FillContractEventArgs>;
expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal(
signedOrder.takerAssetAmount.minus(takerAssetFillAmount),
);
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress].minus(signedOrder.makerAssetAmount),
);
expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerAssetAddress].add(signedOrder.takerAssetAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(signedOrder.makerFee),
);
expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerAssetAddress].minus(signedOrder.takerAssetAmount),
);
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress].add(signedOrder.makerAssetAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(signedOrder.takerFee),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(
signedOrder.makerFee.add(signedOrder.takerFee),
),
);
});
it('should log 1 event with the correct arguments when order has a feeRecipient', async () => {
const divisor = 2;
const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
takerAssetFillAmount: signedOrder.takerAssetAmount.div(divisor),
});
expect(res.logs).to.have.length(1);
const log = res.logs[0] as LogWithDecodedArgs<FillContractEventArgs>;
const logArgs = log.args;
const expectedFilledMakerAssetAmount = signedOrder.makerAssetAmount.div(divisor);
const expectedFilledTakerAssetAmount = signedOrder.takerAssetAmount.div(divisor);
const expectedFeeMPaid = signedOrder.makerFee.div(divisor);
const expectedFeeTPaid = signedOrder.takerFee.div(divisor);
expect(signedOrder.makerAddress).to.be.equal(logArgs.makerAddress);
expect(takerAddress).to.be.equal(logArgs.takerAddress);
expect(takerAddress).to.be.equal(logArgs.senderAddress);
expect(signedOrder.feeRecipientAddress).to.be.equal(logArgs.feeRecipientAddress);
expect(signedOrder.makerAssetData).to.be.equal(logArgs.makerAssetData);
expect(signedOrder.takerAssetData).to.be.equal(logArgs.takerAssetData);
expect(expectedFilledMakerAssetAmount).to.be.bignumber.equal(logArgs.makerAssetFilledAmount);
expect(expectedFilledTakerAssetAmount).to.be.bignumber.equal(logArgs.takerAssetFilledAmount);
expect(expectedFeeMPaid).to.be.bignumber.equal(logArgs.makerFeePaid);
expect(expectedFeeTPaid).to.be.bignumber.equal(logArgs.takerFeePaid);
expect(orderHashUtils.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash);
});
it('should throw when taker is specified and order is claimed by other', async () => {
signedOrder = orderFactory.newSignedOrder({
takerAddress: feeRecipientAddress,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
});
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.InvalidTaker,
);
});
it('should throw if signature is invalid', async () => {
signedOrder = orderFactory.newSignedOrder({
@@ -439,96 +150,6 @@ describe('Exchange core', () => {
);
});
it('should throw if makerAssetAmount is 0', async () => {
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: new BigNumber(0),
});
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.OrderUnfillable,
);
});
it('should throw if takerAssetAmount is 0', async () => {
signedOrder = orderFactory.newSignedOrder({
takerAssetAmount: new BigNumber(0),
});
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.OrderUnfillable,
);
});
it('should throw if takerAssetFillAmount is 0', async () => {
signedOrder = orderFactory.newSignedOrder();
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
takerAssetFillAmount: new BigNumber(0),
}),
RevertReason.InvalidTakerAmount,
);
});
it('should throw if maker erc20Balances are too low to fill order', async () => {
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18),
});
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.TransferFailed,
);
});
it('should throw if taker erc20Balances are too low to fill order', async () => {
signedOrder = orderFactory.newSignedOrder({
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18),
});
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.TransferFailed,
);
});
it('should throw if maker allowances are too low to fill order', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(
await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), {
from: makerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.TransferFailed,
);
});
it('should throw if taker allowances are too low to fill order', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(
await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), {
from: takerAddress,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.TransferFailed,
);
});
it('should throw if an order is expired', async () => {
signedOrder = orderFactory.newSignedOrder({
expirationTimeSeconds: new BigNumber(Math.floor((Date.now() - 10000) / 1000)),
});
return expectRevertReasonOrAlwaysFailingTransactionAsync(
exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
RevertReason.OrderUnfillable,
);
});
it('should throw if no value is filled', async () => {
signedOrder = orderFactory.newSignedOrder();
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
@@ -725,32 +346,6 @@ describe('Exchange core', () => {
});
describe('Testing Exchange of ERC721 Tokens', () => {
it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => {
// Construct Exchange parameters
const makerAssetId = erc721MakerAssetIds[0];
const takerAssetId = erc721TakerAssetIds[1];
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: new BigNumber(1),
takerAssetAmount: new BigNumber(1),
makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
takerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
});
// Verify pre-conditions
const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
// Call Exchange
const takerAssetFillAmount = signedOrder.takerAssetAmount;
// tslint:disable-next-line:no-unused-variable
const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
// Verify post-conditions
const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress);
const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
expect(newOwnerTakerAsset).to.be.bignumber.equal(makerAddress);
});
it('should throw when maker does not own the token with id makerAssetId', async () => {
// Construct Exchange parameters
const makerAssetId = erc721TakerAssetIds[0];
@@ -885,86 +480,6 @@ describe('Exchange core', () => {
RevertReason.LengthGreaterThan131Required,
);
});
it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => {
// Construct Exchange parameters
const makerAssetId = erc721MakerAssetIds[0];
signedOrder = orderFactory.newSignedOrder({
makerAssetAmount: new BigNumber(1),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
takerAssetData: assetProxyUtils.encodeERC20AssetData(defaultTakerAssetAddress),
});
// Verify pre-conditions
const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
// Call Exchange
erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerAssetFillAmount = signedOrder.takerAssetAmount;
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
// Verify ERC721 token was transferred from Maker to Taker
const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress);
// Verify ERC20 tokens were transferred from Taker to Maker & fees were paid correctly
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(signedOrder.makerFee),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(signedOrder.takerFee),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(
signedOrder.makerFee.add(signedOrder.takerFee),
),
);
});
it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => {
// Construct Exchange parameters
const takerAssetId = erc721TakerAssetIds[0];
signedOrder = orderFactory.newSignedOrder({
takerAssetAmount: new BigNumber(1),
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
takerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
makerAssetData: assetProxyUtils.encodeERC20AssetData(defaultMakerAssetAddress),
});
// Verify pre-conditions
const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
// Call Exchange
erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerAssetFillAmount = signedOrder.takerAssetAmount;
await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
// Verify ERC721 token was transferred from Taker to Maker
const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
expect(newOwnerTakerAsset).to.be.bignumber.equal(makerAddress);
// Verify ERC20 tokens were transferred from Maker to Taker & fees were paid correctly
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress].add(signedOrder.makerAssetAmount),
);
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress].minus(signedOrder.makerAssetAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(signedOrder.makerFee),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(signedOrder.takerFee),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(
signedOrder.makerFee.add(signedOrder.takerFee),
),
);
});
});
});
// tslint:disable:max-file-line-count

View File

@@ -3,6 +3,7 @@ import { assetProxyUtils } from '@0xproject/order-utils';
import { AssetProxyId, RevertReason } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
import { ERC20ProxyContract } from '../../src/generated_contract_wrappers/e_r_c20_proxy';
@@ -43,12 +44,13 @@ describe('AssetProxyDispatcher', () => {
before(async () => {
// Setup accounts & addresses
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, notOwner, makerAddress, takerAddress] = accounts);
const usedAddresses = ([owner, notOwner, makerAddress, takerAddress] = _.slice(accounts, 0, 4));
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
[zrxToken] = await erc20Wrapper.deployDummyTokensAsync();
const numDummyErc20ToDeploy = 1;
[zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();

View File

@@ -0,0 +1,306 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils';
import * as _ from 'lodash';
import { chaiSetup } from '../../src/utils/chai_setup';
import { CoreCombinatorialUtils, coreCombinatorialUtilsFactoryAsync } from '../../src/utils/core_combinatorial_utils';
import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper';
import {
AllowanceAmountScenario,
AssetDataScenario,
BalanceAmountScenario,
ExpirationTimeSecondsScenario,
FeeRecipientAddressScenario,
FillScenario,
OrderAssetAmountScenario,
TakerAssetFillAmountScenario,
TakerScenario,
} from '../../src/utils/types';
chaiSetup.configure();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const defaultFillScenario = {
orderScenario: {
takerScenario: TakerScenario.Unspecified,
feeRecipientScenario: FeeRecipientAddressScenario.EthUserAddress,
makerAssetAmountScenario: OrderAssetAmountScenario.Large,
takerAssetAmountScenario: OrderAssetAmountScenario.Large,
makerFeeScenario: OrderAssetAmountScenario.Large,
takerFeeScenario: OrderAssetAmountScenario.Large,
expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InFuture,
makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
makerStateScenario: {
traderAssetBalance: BalanceAmountScenario.Higher,
traderAssetAllowance: AllowanceAmountScenario.Higher,
zrxFeeBalance: BalanceAmountScenario.Higher,
zrxFeeAllowance: AllowanceAmountScenario.Higher,
},
takerStateScenario: {
traderAssetBalance: BalanceAmountScenario.Higher,
traderAssetAllowance: AllowanceAmountScenario.Higher,
zrxFeeBalance: BalanceAmountScenario.Higher,
zrxFeeAllowance: AllowanceAmountScenario.Higher,
},
};
describe('FillOrder Tests', () => {
let coreCombinatorialUtils: CoreCombinatorialUtils;
before(async () => {
await blockchainLifecycle.startAsync();
coreCombinatorialUtils = await coreCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('fillOrder', () => {
const test = (fillScenarios: FillScenario[]) => {
_.forEach(fillScenarios, fillScenario => {
const orderScenario = fillScenario.orderScenario;
const description = `Combinatorial OrderFill: ${orderScenario.feeRecipientScenario} ${
orderScenario.makerAssetAmountScenario
} ${orderScenario.takerAssetAmountScenario} ${orderScenario.makerFeeScenario} ${
orderScenario.takerFeeScenario
} ${orderScenario.expirationTimeSecondsScenario} ${orderScenario.makerAssetDataScenario} ${
orderScenario.takerAssetDataScenario
}`;
it(description, async () => {
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
});
};
const allFillScenarios = CoreCombinatorialUtils.generateFillOrderCombinations();
describe('Combinatorially generated fills orders', () => test(allFillScenarios));
it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
const fillScenario = {
...defaultFillScenario,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
takerAssetAmountScenario: OrderAssetAmountScenario.Small,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetAmountScenario: OrderAssetAmountScenario.Small,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
takerScenario: TakerScenario.CorrectlySpecified,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => {
const fillScenario = {
...defaultFillScenario,
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw when taker is specified and order is claimed by other', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
takerScenario: TakerScenario.IncorrectlySpecified,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if makerAssetAmount is 0', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetAmountScenario: OrderAssetAmountScenario.Zero,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if takerAssetAmount is 0', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
takerAssetAmountScenario: OrderAssetAmountScenario.Zero,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if takerAssetFillAmount is 0', async () => {
const fillScenario = {
...defaultFillScenario,
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if an order is expired', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if maker erc20Balances are too low to fill order', async () => {
const fillScenario = {
...defaultFillScenario,
makerStateScenario: {
...defaultFillScenario.makerStateScenario,
traderAssetBalance: BalanceAmountScenario.TooLow,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if taker erc20Balances are too low to fill order', async () => {
const fillScenario = {
...defaultFillScenario,
takerStateScenario: {
...defaultFillScenario.makerStateScenario,
traderAssetBalance: BalanceAmountScenario.TooLow,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if maker allowances are too low to fill order', async () => {
const fillScenario = {
...defaultFillScenario,
makerStateScenario: {
...defaultFillScenario.makerStateScenario,
traderAssetAllowance: AllowanceAmountScenario.TooLow,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should throw if taker allowances are too low to fill order', async () => {
const fillScenario = {
...defaultFillScenario,
takerStateScenario: {
...defaultFillScenario.makerStateScenario,
traderAssetAllowance: AllowanceAmountScenario.TooLow,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
});
describe('Testing Exchange of ERC721 Tokens', () => {
it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetDataScenario: AssetDataScenario.ERC721,
takerAssetDataScenario: AssetDataScenario.ERC721,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetDataScenario: AssetDataScenario.ERC721,
takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true);
});
it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
takerAssetDataScenario: AssetDataScenario.ERC721,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should successfully fill order when makerAsset is ERC721 and approveAll is set for it', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetDataScenario: AssetDataScenario.ERC721,
takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
makerStateScenario: {
...defaultFillScenario.makerStateScenario,
traderAssetAllowance: AllowanceAmountScenario.Unlimited,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
it('should successfully fill order when makerAsset and takerAsset are ERC721 and approveAll is set for them', async () => {
const fillScenario = {
...defaultFillScenario,
orderScenario: {
...defaultFillScenario.orderScenario,
makerAssetDataScenario: AssetDataScenario.ERC721,
takerAssetDataScenario: AssetDataScenario.ERC721,
},
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
makerStateScenario: {
...defaultFillScenario.makerStateScenario,
traderAssetAllowance: AllowanceAmountScenario.Unlimited,
},
takerStateScenario: {
...defaultFillScenario.takerStateScenario,
traderAssetAllowance: AllowanceAmountScenario.Unlimited,
},
};
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
});
});
});

View File

@@ -76,12 +76,16 @@ describe('matchOrders', () => {
takerAddress,
feeRecipientAddressLeft,
feeRecipientAddressRight,
] = accounts);
] = _.slice(accounts, 0, 6));
// Create wrappers
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
// Deploy ERC20 token & ERC20 proxy
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync();
const numDummyErc20ToDeploy = 3;
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
numDummyErc20ToDeploy,
constants.DUMMY_TOKEN_DECIMALS,
);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
// Deploy ERC721 token and proxy

View File

@@ -3,6 +3,7 @@ import { assetProxyUtils, generatePseudoRandomSalt } from '@0xproject/order-util
import { AssetProxyId, OrderWithoutExchangeAddress, RevertReason, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
import { ERC20ProxyContract } from '../../src/generated_contract_wrappers/e_r_c20_proxy';
@@ -67,11 +68,19 @@ describe('Exchange transactions', () => {
});
before(async () => {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, senderAddress, makerAddress, takerAddress, feeRecipientAddress] = accounts);
const usedAddresses = ([owner, senderAddress, makerAddress, takerAddress, feeRecipientAddress] = _.slice(
accounts,
0,
5,
));
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync();
const numDummyErc20ToDeploy = 3;
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
numDummyErc20ToDeploy,
constants.DUMMY_TOKEN_DECIMALS,
);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();

View File

@@ -60,12 +60,16 @@ describe('Exchange wrappers', () => {
});
before(async () => {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts);
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4));
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync();
const numDummyErc20ToDeploy = 3;
[erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
numDummyErc20ToDeploy,
constants.DUMMY_TOKEN_DECIMALS,
);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
@@ -370,7 +374,7 @@ describe('Exchange wrappers', () => {
// HACK(albrow): We need to hardcode the gas estimate here because
// the Geth gas estimator doesn't work with the way we use
// delegatecall and swallow errors.
gas: 270000,
gas: 280000,
});
// Verify post-conditions
const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);

View File

@@ -3,5 +3,5 @@ import { BigNumber } from '@0xproject/utils';
export abstract class AbstractOrderFilledCancelledFetcher {
public abstract async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber>;
public abstract async isOrderCancelledAsync(orderHash: string): Promise<boolean>;
public abstract getZRXTokenAddress(): string;
public abstract getZRXAssetData(): string;
}

View File

@@ -90,6 +90,9 @@ export class ExchangeTransferSimulator {
amountInBaseUnits: BigNumber,
): Promise<void> {
const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, userAddress);
// HACK: This code assumes that all tokens with an UNLIMITED_ALLOWANCE_IN_BASE_UNITS set,
// are UnlimitedAllowanceTokens. This is however not true, it just so happens that all
// DummyERC20Tokens we use in tests are.
if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
this._store.setProxyAllowance(assetData, userAddress, proxyAllowance.minus(amountInBaseUnits));
}

View File

@@ -16,6 +16,7 @@ export { generatePseudoRandomSalt } from './salt';
export { OrderError, MessagePrefixType, MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types } from './types';
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
export { RemainingFillableCalculator } from './remaining_fillable_calculator';
export { OrderStateUtils } from './order_state_utils';
export { assetProxyUtils } from './asset_proxy_utils';

View File

@@ -10,40 +10,81 @@ import { BigNumber } from '@0xproject/utils';
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
import { assetProxyUtils } from './asset_proxy_utils';
import { orderHashUtils } from './order_hash';
import { RemainingFillableCalculator } from './remaining_fillable_calculator';
import { utils } from './utils';
interface SidedOrderRelevantState {
isMakerSide: boolean;
traderBalance: BigNumber;
traderProxyAllowance: BigNumber;
traderFeeBalance: BigNumber;
traderFeeProxyAllowance: BigNumber;
filledTakerAssetAmount: BigNumber;
remainingFillableAssetAmount: BigNumber;
}
const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001;
export class OrderStateUtils {
private _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher;
private _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher;
private static _validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void {
const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus(orderRelevantState.filledTakerAssetAmount);
private static _validateIfOrderIsValid(
signedOrder: SignedOrder,
sidedOrderRelevantState: SidedOrderRelevantState,
): void {
const isMakerSide = sidedOrderRelevantState.isMakerSide;
const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus(
sidedOrderRelevantState.filledTakerAssetAmount,
);
if (availableTakerAssetAmount.eq(0)) {
throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
}
if (orderRelevantState.makerBalance.eq(0)) {
throw new Error(ExchangeContractErrs.InsufficientMakerBalance);
if (sidedOrderRelevantState.traderBalance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerBalance
: ExchangeContractErrs.InsufficientTakerBalance,
);
}
if (orderRelevantState.makerProxyAllowance.eq(0)) {
throw new Error(ExchangeContractErrs.InsufficientMakerAllowance);
if (sidedOrderRelevantState.traderProxyAllowance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerAllowance
: ExchangeContractErrs.InsufficientTakerAllowance,
);
}
if (!signedOrder.makerFee.eq(0)) {
if (orderRelevantState.makerFeeBalance.eq(0)) {
throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance);
if (sidedOrderRelevantState.traderFeeBalance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerFeeBalance
: ExchangeContractErrs.InsufficientTakerFeeBalance,
);
}
if (orderRelevantState.makerFeeProxyAllowance.eq(0)) {
throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
if (sidedOrderRelevantState.traderFeeProxyAllowance.eq(0)) {
throw new Error(
isMakerSide
? ExchangeContractErrs.InsufficientMakerFeeAllowance
: ExchangeContractErrs.InsufficientTakerFeeAllowance,
);
}
}
const minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount
.dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
.dividedBy(signedOrder.makerAssetAmount);
let minFillableTakerAssetAmountWithinNoRoundingErrorRange;
if (isMakerSide) {
minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount
.dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
.dividedBy(signedOrder.makerAssetAmount);
} else {
minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.makerAssetAmount
.dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
.dividedBy(signedOrder.takerAssetAmount);
}
if (
orderRelevantState.remainingFillableTakerAssetAmount.lessThan(
sidedOrderRelevantState.remainingFillableAssetAmount.lessThan(
minFillableTakerAssetAmountWithinNoRoundingErrorRange,
)
) {
@@ -57,11 +98,20 @@ export class OrderStateUtils {
this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher;
this._orderFilledCancelledFetcher = orderFilledCancelledFetcher;
}
public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder);
public async getOpenOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder);
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const sidedOrderRelevantState = {
isMakerSide: true,
traderBalance: orderRelevantState.makerBalance,
traderProxyAllowance: orderRelevantState.makerProxyAllowance,
traderFeeBalance: orderRelevantState.makerFeeBalance,
traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance,
filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount,
remainingFillableAssetAmount: orderRelevantState.remainingFillableMakerAssetAmount,
};
try {
OrderStateUtils._validateIfOrderIsValid(signedOrder, orderRelevantState);
OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState);
const orderState: OrderStateValid = {
isValid: true,
orderHash,
@@ -77,63 +127,158 @@ export class OrderStateUtils {
return orderState;
}
}
public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> {
const zrxTokenAddress = this._orderFilledCancelledFetcher.getZRXTokenAddress();
const makerProxyData = assetProxyUtils.decodeERC20AssetData(signedOrder.makerAssetData);
const makerAssetAddress = makerProxyData.tokenAddress;
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const makerBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(
makerAssetAddress,
signedOrder.makerAddress,
public async getOpenOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> {
const isMaker = true;
const sidedOrderRelevantState = await this._getSidedOrderRelevantStateAsync(
isMaker,
signedOrder,
signedOrder.takerAddress,
);
const makerProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
makerAssetAddress,
signedOrder.makerAddress,
);
const makerFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(
zrxTokenAddress,
signedOrder.makerAddress,
);
const makerFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
zrxTokenAddress,
signedOrder.makerAddress,
);
const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash);
const totalMakerAssetAmount = signedOrder.makerAssetAmount;
const totalTakerAssetAmount = signedOrder.takerAssetAmount;
const remainingTakerAssetAmount = isOrderCancelled
? new BigNumber(0)
: totalTakerAssetAmount.minus(filledTakerAssetAmount);
const remainingMakerAssetAmount = remainingTakerAssetAmount
.times(totalMakerAssetAmount)
.dividedToIntegerBy(totalTakerAssetAmount);
const transferrableMakerAssetAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
const transferrableFeeAssetAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]);
const zrxAssetData = assetProxyUtils.encodeERC20AssetData(zrxTokenAddress);
const isMakerAssetZRX = signedOrder.makerAssetData === zrxAssetData;
const remainingFillableCalculator = new RemainingFillableCalculator(
signedOrder.makerFee,
signedOrder.makerAssetAmount,
isMakerAssetZRX,
transferrableMakerAssetAmount,
transferrableFeeAssetAmount,
remainingMakerAssetAmount,
);
const remainingFillableMakerAssetAmount = remainingFillableCalculator.computeRemainingFillable();
const remainingFillableTakerAssetAmount = remainingFillableMakerAssetAmount
const remainingFillableTakerAssetAmount = sidedOrderRelevantState.remainingFillableAssetAmount
.times(signedOrder.takerAssetAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const orderRelevantState = {
makerBalance,
makerProxyAllowance,
makerFeeBalance,
makerFeeProxyAllowance,
filledTakerAssetAmount,
remainingFillableMakerAssetAmount,
makerBalance: sidedOrderRelevantState.traderBalance,
makerProxyAllowance: sidedOrderRelevantState.traderProxyAllowance,
makerFeeBalance: sidedOrderRelevantState.traderFeeBalance,
makerFeeProxyAllowance: sidedOrderRelevantState.traderFeeProxyAllowance,
filledTakerAssetAmount: sidedOrderRelevantState.filledTakerAssetAmount,
remainingFillableMakerAssetAmount: sidedOrderRelevantState.remainingFillableAssetAmount,
remainingFillableTakerAssetAmount,
};
return orderRelevantState;
}
public async getMaxFillableTakerAssetAmountAsync(
signedOrder: SignedOrder,
takerAddress: string,
): Promise<BigNumber> {
// Get max fillable amount for an order, considering the makers ability to fill
let isMaker = true;
const orderRelevantMakerState = await this._getSidedOrderRelevantStateAsync(
isMaker,
signedOrder,
signedOrder.takerAddress,
);
const remainingFillableTakerAssetAmountGivenMakersStatus = signedOrder.makerAssetAmount.eq(0)
? new BigNumber(0)
: utils.getPartialAmount(
orderRelevantMakerState.remainingFillableAssetAmount,
signedOrder.makerAssetAmount,
signedOrder.takerAssetAmount,
);
// Get max fillable amount for an order, considering the takers ability to fill
isMaker = false;
const orderRelevantTakerState = await this._getSidedOrderRelevantStateAsync(isMaker, signedOrder, takerAddress);
const remainingFillableTakerAssetAmountGivenTakersStatus = orderRelevantTakerState.remainingFillableAssetAmount;
// The min of these two in the actualy max fillable by either party
const fillableTakerAssetAmount = BigNumber.min([
remainingFillableTakerAssetAmountGivenMakersStatus,
remainingFillableTakerAssetAmountGivenTakersStatus,
]);
return fillableTakerAssetAmount;
}
public async getMaxFillableTakerAssetAmountForFailingOrderAsync(
signedOrder: SignedOrder,
takerAddress: string,
): Promise<BigNumber> {
// Get min of taker balance & allowance
const takerAssetBalanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(
signedOrder.takerAssetData,
takerAddress,
);
const takerAssetAllowanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
signedOrder.takerAssetData,
takerAddress,
);
const minTakerAssetAmount = BigNumber.min([takerAssetBalanceOfTaker, takerAssetAllowanceOfTaker]);
// get remainingFillAmount
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
const remainingFillTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerAssetAmount);
if (minTakerAssetAmount.gte(remainingFillTakerAssetAmount)) {
return remainingFillTakerAssetAmount;
} else {
return minTakerAssetAmount;
}
}
private async _getSidedOrderRelevantStateAsync(
isMakerSide: boolean,
signedOrder: SignedOrder,
takerAddress: string,
): Promise<SidedOrderRelevantState> {
let traderAddress;
let assetData;
let assetAmount;
let feeAmount;
if (isMakerSide) {
traderAddress = signedOrder.makerAddress;
assetData = signedOrder.makerAssetData;
assetAmount = signedOrder.makerAssetAmount;
feeAmount = signedOrder.makerFee;
} else {
traderAddress = takerAddress;
assetData = signedOrder.takerAssetData;
assetAmount = signedOrder.takerAssetAmount;
feeAmount = signedOrder.takerFee;
}
const zrxAssetData = this._orderFilledCancelledFetcher.getZRXAssetData();
const isAssetZRX = assetData === zrxAssetData;
const traderBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress);
const traderProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
assetData,
traderAddress,
);
const traderFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(
zrxAssetData,
traderAddress,
);
const traderFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync(
zrxAssetData,
traderAddress,
);
const transferrableTraderAssetAmount = BigNumber.min([traderProxyAllowance, traderBalance]);
const transferrableFeeAssetAmount = BigNumber.min([traderFeeProxyAllowance, traderFeeBalance]);
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
const totalMakerAssetAmount = signedOrder.makerAssetAmount;
const totalTakerAssetAmount = signedOrder.takerAssetAmount;
const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash);
const remainingTakerAssetAmount = isOrderCancelled
? new BigNumber(0)
: totalTakerAssetAmount.minus(filledTakerAssetAmount);
const remainingMakerAssetAmount = remainingTakerAssetAmount.eq(0)
? new BigNumber(0)
: remainingTakerAssetAmount.times(totalMakerAssetAmount).dividedToIntegerBy(totalTakerAssetAmount);
const remainingAssetAmount = isMakerSide ? remainingMakerAssetAmount : remainingTakerAssetAmount;
const remainingFillableCalculator = new RemainingFillableCalculator(
feeAmount,
assetAmount,
isAssetZRX,
transferrableTraderAssetAmount,
transferrableFeeAssetAmount,
remainingAssetAmount,
);
const remainingFillableAssetAmount = remainingFillableCalculator.computeRemainingFillable();
const sidedOrderRelevantState = {
isMakerSide,
traderBalance,
traderProxyAllowance,
traderFeeBalance,
traderFeeProxyAllowance,
filledTakerAssetAmount,
remainingFillableAssetAmount,
};
return sidedOrderRelevantState;
}
}

View File

@@ -1,24 +1,19 @@
import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types';
import { RevertReason, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import { OrderError, TradeSide, TransferType } from './types';
import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
import { constants } from './constants';
import { ExchangeTransferSimulator } from './exchange_transfer_simulator';
import { ExchangeContract } from './generated_contract_wrappers/exchange';
import { orderHashUtils } from './order_hash';
import { isValidSignatureAsync } from './signature_utils';
import { utils } from './utils';
export class OrderValidationUtils {
private _exchangeContract: ExchangeContract;
// TODO: Write some tests for the function
// const numerator = new BigNumber(20);
// const denominator = new BigNumber(999);
// const target = new BigNumber(50);
// rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1%
private _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher;
public static isRoundingError(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean {
// Solidity's mulmod() in JS
// Source: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions
@@ -36,136 +31,120 @@ export class OrderValidationUtils {
const isError = errPercentageTimes1000000.gt(1000);
return isError;
}
public static validateCancelOrderThrowIfInvalid(
order: Order,
cancelTakerTokenAmount: BigNumber,
filledTakerTokenAmount: BigNumber,
): void {
if (cancelTakerTokenAmount.eq(0)) {
throw new Error(ExchangeContractErrs.OrderCancelAmountZero);
}
if (order.takerAssetAmount.eq(filledTakerTokenAmount)) {
throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
}
const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec();
if (order.expirationTimeSeconds.lessThan(currentUnixTimestampSec)) {
throw new Error(ExchangeContractErrs.OrderCancelExpired);
}
}
public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
exchangeTradeEmulator: ExchangeTransferSimulator,
signedOrder: SignedOrder,
fillTakerTokenAmount: BigNumber,
fillTakerAssetAmount: BigNumber,
senderAddress: string,
zrxTokenAddress: string,
zrxAssetData: string,
): Promise<void> {
const fillMakerTokenAmount = OrderValidationUtils._getPartialAmount(
fillTakerTokenAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
await exchangeTradeEmulator.transferFromAsync(
signedOrder.makerAssetData,
signedOrder.makerAddress,
senderAddress,
fillMakerTokenAmount,
TradeSide.Maker,
TransferType.Trade,
);
await exchangeTradeEmulator.transferFromAsync(
signedOrder.takerAssetData,
senderAddress,
signedOrder.makerAddress,
fillTakerTokenAmount,
TradeSide.Taker,
TransferType.Trade,
);
const makerFeeAmount = OrderValidationUtils._getPartialAmount(
fillTakerTokenAmount,
signedOrder.takerAssetAmount,
signedOrder.makerFee,
);
await exchangeTradeEmulator.transferFromAsync(
zrxTokenAddress,
signedOrder.makerAddress,
signedOrder.feeRecipientAddress,
makerFeeAmount,
TradeSide.Maker,
TransferType.Fee,
);
const takerFeeAmount = OrderValidationUtils._getPartialAmount(
fillTakerTokenAmount,
signedOrder.takerAssetAmount,
signedOrder.takerFee,
);
await exchangeTradeEmulator.transferFromAsync(
zrxTokenAddress,
senderAddress,
signedOrder.feeRecipientAddress,
takerFeeAmount,
TradeSide.Taker,
TransferType.Fee,
);
try {
const fillMakerTokenAmount = utils.getPartialAmount(
fillTakerAssetAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
await exchangeTradeEmulator.transferFromAsync(
signedOrder.makerAssetData,
signedOrder.makerAddress,
senderAddress,
fillMakerTokenAmount,
TradeSide.Maker,
TransferType.Trade,
);
await exchangeTradeEmulator.transferFromAsync(
signedOrder.takerAssetData,
senderAddress,
signedOrder.makerAddress,
fillTakerAssetAmount,
TradeSide.Taker,
TransferType.Trade,
);
const makerFeeAmount = utils.getPartialAmount(
fillTakerAssetAmount,
signedOrder.takerAssetAmount,
signedOrder.makerFee,
);
await exchangeTradeEmulator.transferFromAsync(
zrxAssetData,
signedOrder.makerAddress,
signedOrder.feeRecipientAddress,
makerFeeAmount,
TradeSide.Maker,
TransferType.Fee,
);
const takerFeeAmount = utils.getPartialAmount(
fillTakerAssetAmount,
signedOrder.takerAssetAmount,
signedOrder.takerFee,
);
await exchangeTradeEmulator.transferFromAsync(
zrxAssetData,
senderAddress,
signedOrder.feeRecipientAddress,
takerFeeAmount,
TradeSide.Taker,
TransferType.Fee,
);
} catch (err) {
throw new Error(RevertReason.TransferFailed);
}
}
private static _validateRemainingFillAmountNotZeroOrThrow(
takerAssetAmount: BigNumber,
filledTakerTokenAmount: BigNumber,
): void {
if (takerAssetAmount.eq(filledTakerTokenAmount)) {
throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
throw new Error(RevertReason.OrderUnfillable);
}
}
private static _validateOrderNotExpiredOrThrow(expirationTimeSeconds: BigNumber): void {
const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec();
if (expirationTimeSeconds.lessThan(currentUnixTimestampSec)) {
throw new Error(ExchangeContractErrs.OrderFillExpired);
throw new Error(RevertReason.OrderUnfillable);
}
}
private static _getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
const fillMakerTokenAmount = numerator
.mul(target)
.div(denominator)
.round(0);
return fillMakerTokenAmount;
}
constructor(exchangeContract: ExchangeContract) {
this._exchangeContract = exchangeContract;
constructor(orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher) {
this._orderFilledCancelledFetcher = orderFilledCancelledFetcher;
}
public async validateOrderFillableOrThrowAsync(
exchangeTradeEmulator: ExchangeTransferSimulator,
signedOrder: SignedOrder,
zrxTokenAddress: string,
zrxAssetData: string,
expectedFillTakerTokenAmount?: BigNumber,
): Promise<void> {
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash);
const filledTakerTokenAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow(
signedOrder.takerAssetAmount,
filledTakerTokenAmount,
);
OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds);
let fillTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount);
let fillTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount);
if (!_.isUndefined(expectedFillTakerTokenAmount)) {
fillTakerTokenAmount = expectedFillTakerTokenAmount;
fillTakerAssetAmount = expectedFillTakerTokenAmount;
}
await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
exchangeTradeEmulator,
signedOrder,
fillTakerTokenAmount,
fillTakerAssetAmount,
signedOrder.takerAddress,
zrxTokenAddress,
zrxAssetData,
);
}
public async validateFillOrderThrowIfInvalidAsync(
exchangeTradeEmulator: ExchangeTransferSimulator,
provider: Provider,
signedOrder: SignedOrder,
fillTakerTokenAmount: BigNumber,
fillTakerAssetAmount: BigNumber,
takerAddress: string,
zrxTokenAddress: string,
zrxAssetData: string,
): Promise<BigNumber> {
if (fillTakerTokenAmount.eq(0)) {
throw new Error(ExchangeContractErrs.OrderFillAmountZero);
if (fillTakerAssetAmount.eq(0)) {
throw new Error(RevertReason.InvalidTakerAmount);
}
if (signedOrder.makerAssetAmount.eq(0) || signedOrder.takerAssetAmount.eq(0)) {
throw new Error(RevertReason.OrderUnfillable);
}
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const isValid = await isValidSignatureAsync(
@@ -177,34 +156,34 @@ export class OrderValidationUtils {
if (!isValid) {
throw new Error(OrderError.InvalidSignature);
}
const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash);
const filledTakerTokenAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash);
OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow(
signedOrder.takerAssetAmount,
filledTakerTokenAmount,
);
if (signedOrder.takerAddress !== constants.NULL_ADDRESS && signedOrder.takerAddress !== takerAddress) {
throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
throw new Error(RevertReason.InvalidTaker);
}
OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds);
const remainingTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount);
const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount)
const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerAssetAmount)
? remainingTakerTokenAmount
: fillTakerTokenAmount;
: fillTakerAssetAmount;
await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
exchangeTradeEmulator,
signedOrder,
desiredFillTakerTokenAmount,
takerAddress,
zrxTokenAddress,
zrxAssetData,
);
const wouldRoundingErrorOccur = OrderValidationUtils.isRoundingError(
filledTakerTokenAmount,
desiredFillTakerTokenAmount,
signedOrder.takerAssetAmount,
signedOrder.makerAssetAmount,
);
if (wouldRoundingErrorOccur) {
throw new Error(ExchangeContractErrs.OrderFillRoundingError);
throw new Error(RevertReason.RoundingError);
}
return filledTakerTokenAmount;
}
@@ -212,20 +191,20 @@ export class OrderValidationUtils {
exchangeTradeEmulator: ExchangeTransferSimulator,
provider: Provider,
signedOrder: SignedOrder,
fillTakerTokenAmount: BigNumber,
fillTakerAssetAmount: BigNumber,
takerAddress: string,
zrxTokenAddress: string,
zrxAssetData: string,
): Promise<void> {
const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
exchangeTradeEmulator,
provider,
signedOrder,
fillTakerTokenAmount,
fillTakerAssetAmount,
takerAddress,
zrxTokenAddress,
zrxAssetData,
);
if (filledTakerTokenAmount !== fillTakerTokenAmount) {
throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount);
if (filledTakerTokenAmount !== fillTakerAssetAmount) {
throw new Error(RevertReason.OrderUnfillable);
}
}
}

View File

@@ -23,9 +23,9 @@ export class RemainingFillableCalculator {
this._transferrableAssetAmount = transferrableAssetAmount;
this._transferrableFeeAmount = transferrableFeeAmount;
this._remainingOrderAssetAmount = remainingOrderAssetAmount;
this._remainingOrderFeeAmount = remainingOrderAssetAmount
.times(this._orderFee)
.dividedToIntegerBy(this._orderAssetAmount);
this._remainingOrderFeeAmount = orderAssetAmount.eq(0)
? new BigNumber(0)
: remainingOrderAssetAmount.times(orderFee).dividedToIntegerBy(orderAssetAmount);
}
public computeRemainingFillable(): BigNumber {
if (this._hasSufficientFundsForFeeAndTransferAmount()) {

View File

@@ -19,8 +19,8 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx
[userAddress: string]: BigNumber;
};
};
constructor(token: AbstractBalanceAndProxyAllowanceFetcher) {
this._balanceAndProxyAllowanceFetcher = token;
constructor(balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher) {
this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher;
this._balance = {};
this._proxyAllowance = {};
}

View File

@@ -12,4 +12,11 @@ export const utils = {
const milisecondsInSecond = 1000;
return new BigNumber(Date.now() / milisecondsInSecond).round();
},
getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
const fillMakerTokenAmount = numerator
.mul(target)
.div(denominator)
.round(0);
return fillMakerTokenAmount;
},
};

View File

@@ -4,6 +4,7 @@ import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { artifacts } from '../src/artifacts';
import { assetProxyUtils } from '../src/asset_proxy_utils';
import { constants } from '../src/constants';
import { ExchangeTransferSimulator } from '../src/exchange_transfer_simulator';
import { DummyERC20TokenContract } from '../src/generated_contract_wrappers/dummy_e_r_c20_token';
@@ -27,7 +28,7 @@ describe('ExchangeTransferSimulator', async () => {
let coinbase: string;
let sender: string;
let recipient: string;
let exampleTokenAddress: string;
let exampleAssetData: string;
let exchangeTransferSimulator: ExchangeTransferSimulator;
let txHash: string;
let erc20ProxyAddress: string;
@@ -65,7 +66,7 @@ describe('ExchangeTransferSimulator', async () => {
totalSupply,
);
exampleTokenAddress = dummyERC20Token.address;
exampleAssetData = assetProxyUtils.encodeERC20AssetData(dummyERC20Token.address);
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
@@ -91,7 +92,7 @@ describe('ExchangeTransferSimulator', async () => {
it("throws if the user doesn't have enough allowance", async () => {
return expect(
exchangeTransferSimulator.transferFromAsync(
exampleTokenAddress,
exampleAssetData,
sender,
recipient,
transferAmount,
@@ -107,7 +108,7 @@ describe('ExchangeTransferSimulator', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
return expect(
exchangeTransferSimulator.transferFromAsync(
exampleTokenAddress,
exampleAssetData,
sender,
recipient,
transferAmount,
@@ -128,7 +129,7 @@ describe('ExchangeTransferSimulator', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await exchangeTransferSimulator.transferFromAsync(
exampleTokenAddress,
exampleAssetData,
sender,
recipient,
transferAmount,
@@ -136,9 +137,9 @@ describe('ExchangeTransferSimulator', async () => {
TransferType.Trade,
);
const store = (exchangeTransferSimulator as any)._store;
const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
const senderBalance = await store.getBalanceAsync(exampleAssetData, sender);
const recipientBalance = await store.getBalanceAsync(exampleAssetData, recipient);
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleAssetData, sender);
expect(senderBalance).to.be.bignumber.equal(0);
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
expect(senderProxyAllowance).to.be.bignumber.equal(0);
@@ -157,7 +158,7 @@ describe('ExchangeTransferSimulator', async () => {
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
await exchangeTransferSimulator.transferFromAsync(
exampleTokenAddress,
exampleAssetData,
sender,
recipient,
transferAmount,
@@ -165,9 +166,9 @@ describe('ExchangeTransferSimulator', async () => {
TransferType.Trade,
);
const store = (exchangeTransferSimulator as any)._store;
const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
const senderBalance = await store.getBalanceAsync(exampleAssetData, sender);
const recipientBalance = await store.getBalanceAsync(exampleAssetData, recipient);
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleAssetData, sender);
expect(senderBalance).to.be.bignumber.equal(0);
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
expect(senderProxyAllowance).to.be.bignumber.equal(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);

View File

@@ -6,7 +6,7 @@ import { provider } from './utils/web3_wrapper';
before('migrate contracts', async function(): Promise<void> {
// HACK: Since the migrations take longer then our global mocha timeout limit
// we manually increase it for this before hook.
const mochaTestTimeoutMs = 20000;
const mochaTestTimeoutMs = 25000;
this.timeout(mochaTestTimeoutMs);
const txDefaults = {
gas: devConstants.GAS_LIMIT,

View File

@@ -30,6 +30,7 @@ import {
import { BigNumber, intervalUtils, logUtils, promisify } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import * as moment from 'moment';
import * as React from 'react';
import contract = require('truffle-contract');
import { BlockchainWatcher } from 'ts/blockchain_watcher';
@@ -43,6 +44,8 @@ import {
BlockchainErrs,
ContractInstance,
Fill,
InjectedProviderObservable,
InjectedProviderUpdate,
Order as PortalOrder,
Providers,
ProviderType,
@@ -79,9 +82,9 @@ export class Blockchain {
private _dispatcher: Dispatcher;
private _web3Wrapper?: Web3Wrapper;
private _blockchainWatcher?: BlockchainWatcher;
private _injectedProviderObservable?: InjectedProviderObservable;
private _injectedProviderUpdateHandler: (update: InjectedProviderUpdate) => Promise<void>;
private _userAddressIfExists: string;
private _cachedProvider: Provider;
private _cachedProviderNetworkId: number;
private _ledgerSubprovider: LedgerSubprovider;
private _defaultGasPrice: BigNumber;
private static _getNameGivenProvider(provider: Provider): string {
@@ -92,16 +95,62 @@ export class Blockchain {
}
return providerNameIfExists;
}
private static async _getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number): Promise<Provider> {
private static _getInjectedWeb3(): any {
return (window as any).web3;
}
private static async _getInjectedWeb3ProviderNetworkIdIfExistsAsync(): Promise<number | undefined> {
// Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in
// order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot
// retrieve it from within the web3Wrapper constructor. This is and should remain the only
// call to a web3 instance outside of web3Wrapper in the entire dapp.
// In addition, if the user has an injectedWeb3 instance that is disconnected from a backing
// Ethereum node, this call will throw. We need to handle this case gracefully
const injectedWeb3IfExists = Blockchain._getInjectedWeb3();
let networkIdIfExists: number;
if (!_.isUndefined(injectedWeb3IfExists)) {
try {
networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3IfExists.version.getNetwork)());
} catch (err) {
// Ignore error and proceed with networkId undefined
}
}
return networkIdIfExists;
}
private static async _getProviderAsync(
injectedWeb3: Web3,
networkIdIfExists: number,
shouldUserLedgerProvider: boolean = false,
): Promise<[Provider, LedgerSubprovider | undefined]> {
const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3);
const isNetworkIdAvailable = !_.isUndefined(networkIdIfExists);
const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists];
const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId);
let provider;
if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) {
if (shouldUserLedgerProvider && isNetworkIdAvailable) {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
const provider = new ProviderEngine();
const ledgerWalletConfigs = {
networkId: networkIdIfExists,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
const ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists], publicNodeUrl => {
return new RpcSubprovider({
rpcUrl: publicNodeUrl,
});
});
provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
provider.start();
return [provider, ledgerSubprovider];
} else if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) {
// We catch all requests involving a users account and send it to the injectedWeb3
// instance. All other requests go to the public hosted node.
provider = new ProviderEngine();
const provider = new ProviderEngine();
provider.addProvider(new InjectedWeb3Subprovider(injectedWeb3.currentProvider));
provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => {
@@ -111,16 +160,17 @@ export class Blockchain {
});
provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
provider.start();
return [provider, undefined];
} else if (doesInjectedWeb3Exist) {
// Since no public node for this network, all requests go to injectedWeb3 instance
provider = injectedWeb3.currentProvider;
return [injectedWeb3.currentProvider, undefined];
} else {
// If no injectedWeb3 instance, all requests fallback to our public hosted mainnet/testnet node
// We do this so that users can still browse the 0x Portal DApp even if they do not have web3
// injected into their browser.
provider = new ProviderEngine();
const provider = new ProviderEngine();
provider.addProvider(new FilterSubprovider());
const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN;
const networkId = constants.NETWORK_ID_MAINNET;
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => {
return new RpcSubprovider({
rpcUrl: publicNodeUrl,
@@ -128,14 +178,15 @@ export class Blockchain {
});
provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
provider.start();
return [provider, undefined];
}
return provider;
}
constructor(dispatcher: Dispatcher) {
this._dispatcher = dispatcher;
const defaultGasPrice = GWEI_IN_WEI * 30;
this._defaultGasPrice = new BigNumber(defaultGasPrice);
// We need a unique reference to this function so we can use it to unsubcribe.
this._injectedProviderUpdateHandler = this._handleInjectedProviderUpdateAsync.bind(this);
// tslint:disable-next-line:no-floating-promises
this._updateDefaultGasPriceAsync();
// tslint:disable-next-line:no-floating-promises
@@ -185,84 +236,17 @@ export class Blockchain {
this._ledgerSubprovider.setPath(path);
}
public async updateProviderToLedgerAsync(networkId: number): Promise<void> {
utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.');
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
// Cache injected provider so that we can switch the user back to it easily
if (_.isUndefined(this._cachedProvider)) {
this._cachedProvider = this._web3Wrapper.getProvider();
this._cachedProviderNetworkId = this.networkId;
}
this._blockchainWatcher.destroy();
delete this._userAddressIfExists;
this._dispatcher.updateUserAddress(undefined); // Clear old userAddress
const provider = new ProviderEngine();
const ledgerWalletConfigs = {
networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this._ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => {
return new RpcSubprovider({
rpcUrl: publicNodeUrl,
});
});
provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
provider.start();
this.networkId = networkId;
this._dispatcher.updateNetworkId(this.networkId);
const shouldPollUserAddress = false;
this._web3Wrapper = new Web3Wrapper(provider);
this._blockchainWatcher = new BlockchainWatcher(
this._dispatcher,
this._web3Wrapper,
this.networkId,
shouldPollUserAddress,
);
this._contractWrappers.setProvider(provider, this.networkId);
await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync();
this._dispatcher.updateProviderType(ProviderType.Ledger);
const shouldUserLedgerProvider = true;
await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider);
}
public async updateProviderToInjectedAsync(): Promise<void> {
utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.');
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
this._blockchainWatcher.destroy();
const provider = this._cachedProvider;
this.networkId = this._cachedProviderNetworkId;
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(provider);
this._blockchainWatcher = new BlockchainWatcher(
this._dispatcher,
this._web3Wrapper,
this.networkId,
shouldPollUserAddress,
);
const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
this._userAddressIfExists = userAddresses[0];
this._contractWrappers.setProvider(provider, this.networkId);
await this.fetchTokenInformationAsync();
await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync();
this._dispatcher.updateProviderType(ProviderType.Injected);
delete this._ledgerSubprovider;
delete this._cachedProvider;
const shouldUserLedgerProvider = false;
this._dispatcher.updateBlockchainIsLoaded(false);
// We don't want to be out of sync with the network the injected provider declares.
const networkId = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync();
await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider);
}
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
@@ -538,6 +522,7 @@ export class Blockchain {
}
public destroy(): void {
this._blockchainWatcher.destroy();
this._injectedProviderObservable.unsubscribe(this._injectedProviderUpdateHandler);
this._stopWatchingExchangeLogFillEvents();
}
public async fetchTokenInformationAsync(): Promise<void> {
@@ -559,6 +544,7 @@ export class Blockchain {
tokenRegistryTokenSymbols,
configs.DEFAULT_TRACKED_TOKEN_SYMBOLS,
);
const currentTimestamp = moment().unix();
if (defaultTrackedTokensInRegistry.length !== configs.DEFAULT_TRACKED_TOKEN_SYMBOLS.length) {
this._dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
this._dispatcher.encounteredBlockchainError(BlockchainErrs.DefaultTokensNotInTokenRegistry);
@@ -573,7 +559,7 @@ export class Blockchain {
if (_.isEmpty(trackedTokensByAddress)) {
_.each(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
token.isTracked = true;
token.trackedTimestamp = currentTimestamp;
trackedTokensByAddress[token.address] = token;
});
if (!_.isUndefined(this._userAddressIfExists)) {
@@ -582,10 +568,10 @@ export class Blockchain {
});
}
} else {
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
_.each(trackedTokensByAddress, (_trackedToken: Token, address: string) => {
// Properly set all tokenRegistry tokens `trackedTimestamp` if they are in the existing trackedTokens array
_.each(trackedTokensByAddress, (trackedToken: Token, address: string) => {
if (!_.isUndefined(tokenRegistryTokensByAddress[address])) {
tokenRegistryTokensByAddress[address].isTracked = true;
tokenRegistryTokensByAddress[address].trackedTimestamp = trackedToken.trackedTimestamp;
}
});
}
@@ -632,6 +618,18 @@ export class Blockchain {
private _doesUserAddressExist(): boolean {
return !_.isUndefined(this._userAddressIfExists);
}
private async _handleInjectedProviderUpdateAsync(update: InjectedProviderUpdate): Promise<void> {
if (update.networkVersion === 'loading' || !_.isUndefined(this._ledgerSubprovider)) {
return;
}
const updatedNetworkId = _.parseInt(update.networkVersion);
if (this.networkId === updatedNetworkId) {
return;
}
const shouldPollUserAddress = true;
const shouldUserLedgerProvider = false;
await this._resetOrInitializeAsync(updatedNetworkId, shouldPollUserAddress, shouldUserLedgerProvider);
}
private async _rehydrateStoreWithContractEventsAsync(): Promise<void> {
// Ensure we are only ever listening to one set of events
this._stopWatchingExchangeLogFillEvents();
@@ -765,7 +763,7 @@ export class Blockchain {
name: t.name,
symbol: t.symbol,
decimals: t.decimals,
isTracked: false,
trackedTimestamp: undefined,
isRegistered: true,
};
tokenByAddress[token.address] = token;
@@ -774,49 +772,64 @@ export class Blockchain {
}
private async _onPageLoadInitFireAndForgetAsync(): Promise<void> {
await utils.onPageLoadAsync(); // wait for page to load
// Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in
// order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot
// retrieve it from within the web3Wrapper constructor. This is and should remain the only
// call to a web3 instance outside of web3Wrapper in the entire dapp.
// In addition, if the user has an injectedWeb3 instance that is disconnected from a backing
// Ethereum node, this call will throw. We need to handle this case gracefully
const injectedWeb3 = (window as any).web3;
let networkIdIfExists: number;
if (!_.isUndefined(injectedWeb3)) {
try {
networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3.version.getNetwork)());
} catch (err) {
// Ignore error and proceed with networkId undefined
const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync();
this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : constants.NETWORK_ID_MAINNET;
const injectedWeb3IfExists = Blockchain._getInjectedWeb3();
if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) {
const injectedProviderObservable = injectedWeb3IfExists.currentProvider.publicConfigStore;
if (!_.isUndefined(injectedProviderObservable) && _.isUndefined(this._injectedProviderObservable)) {
this._injectedProviderObservable = injectedProviderObservable;
this._injectedProviderObservable.subscribe(this._injectedProviderUpdateHandler);
}
}
const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists);
this.networkId = !_.isUndefined(networkIdIfExists)
? networkIdIfExists
: configs.IS_MAINNET_ENABLED
? constants.NETWORK_ID_MAINNET
: constants.NETWORK_ID_KOVAN;
this._dispatcher.updateNetworkId(this.networkId);
const zeroExConfigs = {
networkId: this.networkId,
};
this._contractWrappers = new ContractWrappers(provider, zeroExConfigs);
this._updateProviderName(injectedWeb3);
this._updateProviderName(injectedWeb3IfExists);
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(provider);
this._blockchainWatcher = new BlockchainWatcher(
this._dispatcher,
this._web3Wrapper,
this.networkId,
shouldPollUserAddress,
const shouldUseLedgerProvider = false;
await this._resetOrInitializeAsync(this.networkId, shouldPollUserAddress, shouldUseLedgerProvider);
}
private async _resetOrInitializeAsync(
networkId: number,
shouldPollUserAddress: boolean = false,
shouldUserLedgerProvider: boolean = false,
): Promise<void> {
if (!shouldUserLedgerProvider) {
this._dispatcher.updateBlockchainIsLoaded(false);
}
this._dispatcher.updateUserWeiBalance(undefined);
this.networkId = networkId;
const injectedWeb3IfExists = Blockchain._getInjectedWeb3();
const [provider, ledgerSubproviderIfExists] = await Blockchain._getProviderAsync(
injectedWeb3IfExists,
networkId,
shouldUserLedgerProvider,
);
const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
this._userAddressIfExists = userAddresses[0];
this._dispatcher.updateUserAddress(this._userAddressIfExists);
await this.fetchTokenInformationAsync();
await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync();
if (!_.isUndefined(this._contractWrappers)) {
this._contractWrappers.setProvider(provider, networkId);
} else {
this._contractWrappers = new ContractWrappers(provider, { networkId });
}
if (!_.isUndefined(this._blockchainWatcher)) {
this._blockchainWatcher.destroy();
}
this._web3Wrapper = new Web3Wrapper(provider);
this._blockchainWatcher = new BlockchainWatcher(this._dispatcher, this._web3Wrapper, shouldPollUserAddress);
if (shouldUserLedgerProvider && !_.isUndefined(ledgerSubproviderIfExists)) {
delete this._userAddressIfExists;
this._ledgerSubprovider = ledgerSubproviderIfExists;
this._dispatcher.updateUserAddress(undefined);
this._dispatcher.updateProviderType(ProviderType.Ledger);
} else {
delete this._ledgerSubprovider;
const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
this._userAddressIfExists = userAddresses[0];
this._dispatcher.updateUserAddress(this._userAddressIfExists);
if (!_.isUndefined(injectedWeb3IfExists)) {
this._dispatcher.updateProviderType(ProviderType.Injected);
}
await this.fetchTokenInformationAsync();
}
await this._blockchainWatcher.startEmittingUserBalanceStateAsync();
this._dispatcher.updateNetworkId(networkId);
await this._rehydrateStoreWithContractEventsAsync();
}
private _updateProviderName(injectedWeb3: Web3): void {

View File

@@ -6,24 +6,17 @@ import { Dispatcher } from 'ts/redux/dispatcher';
export class BlockchainWatcher {
private _dispatcher: Dispatcher;
private _web3Wrapper: Web3Wrapper;
private _prevNetworkId: number;
private _shouldPollUserAddress: boolean;
private _watchNetworkAndBalanceIntervalId: NodeJS.Timer;
private _watchBalanceIntervalId: NodeJS.Timer;
private _prevUserEtherBalanceInWei?: BigNumber;
private _prevUserAddressIfExists: string;
constructor(
dispatcher: Dispatcher,
web3Wrapper: Web3Wrapper,
networkIdIfExists: number,
shouldPollUserAddress: boolean,
) {
constructor(dispatcher: Dispatcher, web3Wrapper: Web3Wrapper, shouldPollUserAddress: boolean) {
this._dispatcher = dispatcher;
this._prevNetworkId = networkIdIfExists;
this._shouldPollUserAddress = shouldPollUserAddress;
this._web3Wrapper = web3Wrapper;
}
public destroy(): void {
this._stopEmittingNetworkConnectionAndUserBalanceState();
this._stopEmittingUserBalanceState();
// HACK: stop() is only available on providerEngine instances
const provider = this._web3Wrapper.getProvider();
if (!_.isUndefined((provider as any).stop)) {
@@ -34,36 +27,23 @@ export class BlockchainWatcher {
public updatePrevUserAddress(userAddress: string): void {
this._prevUserAddressIfExists = userAddress;
}
public async startEmittingNetworkConnectionAndUserBalanceStateAsync(): Promise<void> {
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
public async startEmittingUserBalanceStateAsync(): Promise<void> {
if (!_.isUndefined(this._watchBalanceIntervalId)) {
return; // we are already emitting the state
}
this._prevUserEtherBalanceInWei = undefined;
this._dispatcher.updateNetworkId(this._prevNetworkId);
await this._updateNetworkAndBalanceAsync();
this._watchNetworkAndBalanceIntervalId = intervalUtils.setAsyncExcludingInterval(
this._updateNetworkAndBalanceAsync.bind(this),
await this._updateBalanceAsync();
this._watchBalanceIntervalId = intervalUtils.setAsyncExcludingInterval(
this._updateBalanceAsync.bind(this),
5000,
(err: Error) => {
logUtils.log(`Watching network and balances failed: ${err.stack}`);
this._stopEmittingNetworkConnectionAndUserBalanceState();
this._stopEmittingUserBalanceState();
},
);
}
private async _updateNetworkAndBalanceAsync(): Promise<void> {
// Check for network state changes
private async _updateBalanceAsync(): Promise<void> {
let prevNodeVersion: string;
let currentNetworkId;
try {
currentNetworkId = await this._web3Wrapper.getNetworkIdAsync();
} catch (err) {
// Noop
}
if (currentNetworkId !== this._prevNetworkId) {
this._prevNetworkId = currentNetworkId;
this._dispatcher.updateNetworkId(currentNetworkId);
}
// Check for node version changes
const currentNodeVersion = await this._web3Wrapper.getNodeVersionAsync();
if (currentNodeVersion !== prevNodeVersion) {
@@ -99,9 +79,9 @@ export class BlockchainWatcher {
this._dispatcher.updateUserWeiBalance(balanceInWei);
}
}
private _stopEmittingNetworkConnectionAndUserBalanceState(): void {
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
intervalUtils.clearAsyncExcludingInterval(this._watchNetworkAndBalanceIntervalId);
private _stopEmittingUserBalanceState(): void {
if (!_.isUndefined(this._watchBalanceIntervalId)) {
intervalUtils.clearAsyncExcludingInterval(this._watchBalanceIntervalId);
}
}
}

View File

@@ -4,7 +4,6 @@ import FlatButton from 'material-ui/FlatButton';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { BlockchainErrs } from 'ts/types';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
interface BlockchainErrDialogProps {
@@ -125,8 +124,7 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp
Parity Signer Chrome extension
</a>{' '}
lets you connect to a locally running Parity node. Make sure you have started your local Parity node
with {configs.IS_MAINNET_ENABLED && '`parity ui` or'} `parity --chain kovan ui` in order to connect
to {configs.IS_MAINNET_ENABLED ? 'mainnet or Kovan respectively.' : 'Kovan.'}
with `parity ui` or `parity --chain kovan ui` in order to connect to mainnet or Kovan respectively.
</div>
<div className="pt2">
<span className="bold">Note:</span> If you have done one of the above steps and are still seeing
@@ -142,10 +140,8 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp
<div>
The 0x smart contracts are not deployed on the Ethereum network you are currently connected to
(network Id: {this.props.networkId}). In order to use the 0x portal dApp, please connect to the{' '}
{Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN})
{configs.IS_MAINNET_ENABLED
? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).`
: `.`}
{Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN}) or ${constants.MAINNET_NAME}{' '}
(network Id: ${constants.NETWORK_ID_MAINNET}).
</div>
<h4>Metamask</h4>
<div>
@@ -159,11 +155,8 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp
If using the{' '}
<a href={constants.URL_PARITY_CHROME_STORE} target="_blank">
Parity Signer Chrome extension
</a>, make sure to start your local Parity node with{' '}
{configs.IS_MAINNET_ENABLED
? '`parity ui` or `parity --chain Kovan ui` in order to connect to mainnet \
or Kovan respectively.'
: '`parity --chain kovan ui` in order to connect to Kovan.'}
</a>, make sure to start your local Parity node with `parity ui` or `parity --chain Kovan ui` in
order to connect to mainnet \ or Kovan respectively.
</div>
</div>
);

View File

@@ -282,6 +282,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
if (didSucceed) {
this.setState({
stepIndex: LedgerSteps.SELECT_ADDRESS,
connectionErrMsg: '',
});
}
return didSucceed;

View File

@@ -1,5 +1,6 @@
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import * as moment from 'moment';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation';
@@ -73,12 +74,13 @@ export class TrackTokenConfirmationDialog extends React.Component<
this.setState({
isAddingTokenToTracked: true,
});
const currentTimestamp = moment().unix();
for (const token of this.props.tokens) {
const newTokenEntry = {
...token,
trackedTimestamp: currentTimestamp,
};
newTokenEntry.isTracked = true;
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
}

View File

@@ -373,26 +373,26 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
const tokensToTrack: Token[] = [];
const isUnseenMakerToken = _.isUndefined(makerTokenIfExists);
const isMakerTokenTracked = !_.isUndefined(makerTokenIfExists) && makerTokenIfExists.isTracked;
const isMakerTokenTracked = !_.isUndefined(makerTokenIfExists) && utils.isTokenTracked(makerTokenIfExists);
if (isUnseenMakerToken) {
tokensToTrack.push({
...this.state.parsedOrder.metadata.makerToken,
address: this.state.parsedOrder.signedOrder.makerTokenAddress,
iconUrl: undefined,
isTracked: false,
trackedTimestamp: undefined,
isRegistered: false,
});
} else if (!isMakerTokenTracked) {
tokensToTrack.push(makerTokenIfExists);
}
const isUnseenTakerToken = _.isUndefined(takerTokenIfExists);
const isTakerTokenTracked = !_.isUndefined(takerTokenIfExists) && takerTokenIfExists.isTracked;
const isTakerTokenTracked = !_.isUndefined(takerTokenIfExists) && utils.isTokenTracked(takerTokenIfExists);
if (isUnseenTakerToken) {
tokensToTrack.push({
...this.state.parsedOrder.metadata.takerToken,
address: this.state.parsedOrder.signedOrder.takerTokenAddress,
iconUrl: undefined,
isTracked: false,
trackedTimestamp: undefined,
isRegistered: false,
});
} else if (!isTakerTokenTracked) {

View File

@@ -1,6 +1,7 @@
import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import * as moment from 'moment';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { NewTokenForm } from 'ts/components/generate_order/new_token_form';
@@ -10,6 +11,7 @@ import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
const TOKEN_ICON_DIMENSION = 100;
const TILE_DIMENSION = 146;
@@ -134,8 +136,8 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
let tileStyles;
const gridTiles = _.map(this.props.tokenByAddress, (token: Token, address: string) => {
if (
(this.props.tokenVisibility === TokenVisibility.TRACKED && !token.isTracked) ||
(this.props.tokenVisibility === TokenVisibility.UNTRACKED && token.isTracked) ||
(this.props.tokenVisibility === TokenVisibility.TRACKED && !utils.isTokenTracked(token)) ||
(this.props.tokenVisibility === TokenVisibility.UNTRACKED && utils.isTokenTracked(token)) ||
token.symbol === constants.ZRX_TOKEN_SYMBOL ||
token.symbol === constants.ETHER_TOKEN_SYMBOL
) {
@@ -212,7 +214,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
}
private _onChooseToken(tokenAddress: string): void {
const token = this.props.tokenByAddress[tokenAddress];
if (token.isTracked) {
if (utils.isTokenTracked(token)) {
this.props.onTokenChosen(tokenAddress);
} else {
this.setState({
@@ -257,9 +259,8 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
}
const newTokenEntry = {
...token,
trackedTimestamp: moment().unix(),
};
newTokenEntry.isTracked = true;
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
this.props.dispatcher.updateTokenByAddress([newTokenEntry]);

View File

@@ -1,6 +1,7 @@
import { colors } from '@0xproject/react-shared';
import * as _ from 'lodash';
import TextField from 'material-ui/TextField';
import * as moment from 'moment';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { AddressInput } from 'ts/components/inputs/address_input';
@@ -147,7 +148,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor
iconUrl: undefined,
name: this.state.name,
symbol: this.state.symbol.toUpperCase(),
isTracked: true,
trackedTimestamp: moment().unix(),
isRegistered: false,
};
this.props.onNewTokenSubmitted(newToken);

View File

@@ -23,7 +23,6 @@ import { GenerateOrderForm } from 'ts/containers/generate_order_form';
import { localStorage } from 'ts/local_storage/local_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { orderParser } from 'ts/utils/order_parser';
import { Translate } from 'ts/utils/translate';
@@ -170,67 +169,53 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta
/>
<div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}>
<Paper className="mb3 mt2">
{!configs.IS_MAINNET_ENABLED && this.props.networkId === constants.NETWORK_ID_MAINNET ? (
<div className="p3 center">
<div className="h2 py2">Mainnet unavailable</div>
<div className="mx-auto pb2 pt2">
<img src="/images/zrx_token.png" style={{ width: 150 }} />
</div>
<div>
0x portal is currently unavailable on the Ethereum mainnet.
<div>To try it out, switch to the Kovan test network (networkId: 42).</div>
<div className="py2">Check back soon!</div>
</div>
<div className="mx-auto flex">
<div className="col col-2 pr2 pt1 sm-hide xs-hide" style={portalMenuContainerStyle}>
<LegacyPortalMenu menuItemStyle={{ color: colors.white }} />
</div>
) : (
<div className="mx-auto flex">
<div className="col col-2 pr2 pt1 sm-hide xs-hide" style={portalMenuContainerStyle}>
<LegacyPortalMenu menuItemStyle={{ color: colors.white }} />
</div>
<div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12">
<div className="py2" style={{ backgroundColor: colors.grey50 }}>
{this.props.blockchainIsLoaded ? (
<Switch>
<Route
path={`${WebsitePaths.Portal}/weth`}
render={this._renderEthWrapper.bind(this)}
/>
<Route
path={`${WebsitePaths.Portal}/fill`}
render={this._renderFillOrder.bind(this)}
/>
<Route
path={`${WebsitePaths.Portal}/balances`}
render={this._renderTokenBalances.bind(this)}
/>
<Route
path={`${WebsitePaths.Portal}/trades`}
render={this._renderTradeHistory.bind(this)}
/>
<Route
path={`${WebsitePaths.Home}`}
render={this._renderGenerateOrderForm.bind(this)}
/>
</Switch>
) : (
<div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}>
<div
className="relative sm-px2 sm-pt2 sm-m1"
style={{ height: 122, top: '50%', transform: 'translateY(-50%)' }}
>
<div className="center pb2">
<CircularProgress size={40} thickness={5} />
</div>
<div className="center pt2" style={{ paddingBottom: 11 }}>
Loading Portal...
</div>
<div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12">
<div className="py2" style={{ backgroundColor: colors.grey50 }}>
{this.props.blockchainIsLoaded ? (
<Switch>
<Route
path={`${WebsitePaths.Portal}/weth`}
render={this._renderEthWrapper.bind(this)}
/>
<Route
path={`${WebsitePaths.Portal}/fill`}
render={this._renderFillOrder.bind(this)}
/>
<Route
path={`${WebsitePaths.Portal}/balances`}
render={this._renderTokenBalances.bind(this)}
/>
<Route
path={`${WebsitePaths.Portal}/trades`}
render={this._renderTradeHistory.bind(this)}
/>
<Route
path={`${WebsitePaths.Home}`}
render={this._renderGenerateOrderForm.bind(this)}
/>
</Switch>
) : (
<div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}>
<div
className="relative sm-px2 sm-pt2 sm-m1"
style={{ height: 122, top: '50%', transform: 'translateY(-50%)' }}
>
<div className="center pb2">
<CircularProgress size={40} thickness={5} />
</div>
<div className="center pt2" style={{ paddingBottom: 11 }}>
Loading Portal...
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
</Paper>
<BlockchainErrDialog
blockchain={this._blockchain}
@@ -249,16 +234,14 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
/>
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
{this.props.blockchainIsLoaded && (
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this.onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
)}
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this.onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
</div>
<Footer translate={this.props.translate} dispatcher={this.props.dispatcher} />
</div>
@@ -292,8 +275,7 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta
);
}
private _renderTokenBalances(): React.ReactNode {
const allTokens = _.values(this.props.tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
const trackedTokens = utils.getTrackedTokens(this.props.tokenByAddress);
return (
<TokenBalances
blockchain={this._blockchain}

View File

@@ -186,9 +186,11 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
const ethToken = utils.getEthToken(this.props.tokenByAddress);
const zrxToken = utils.getZrxToken(this.props.tokenByAddress);
if (ethToken && zrxToken) {
const ethTokenAllowance = this.props.trackedTokenStateByAddress[ethToken.address].allowance;
const zrxTokenAllowance = this.props.trackedTokenStateByAddress[zrxToken.address].allowance;
return ethTokenAllowance > new BigNumber(0) && zrxTokenAllowance > new BigNumber(0);
const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address];
const zrxTokenState = this.props.trackedTokenStateByAddress[zrxToken.address];
if (ethTokenState && zrxTokenState) {
return ethTokenState.allowance.gt(0) && zrxTokenState.allowance.gt(0);
}
}
return false;
}
@@ -251,12 +253,15 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
if (!token) {
return null;
}
const tokenState = this.props.trackedTokenStateByAddress[token.address];
const tokenStateIfExists = this.props.trackedTokenStateByAddress[token.address];
if (_.isUndefined(tokenStateIfExists)) {
return null;
}
return (
<AllowanceToggle
token={token}
tokenState={tokenState}
isDisabled={!tokenState.isLoaded}
tokenState={tokenStateIfExists}
isDisabled={!tokenStateIfExists.isLoaded}
blockchain={this.props.blockchain}
// tslint:disable-next-line:jsx-no-lambda
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(token.address)}

View File

@@ -152,9 +152,8 @@ export class Portal extends React.Component<PortalProps, PortalState> {
}
public componentDidUpdate(prevProps: PortalProps): void {
if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses());
}
}
public componentWillReceiveProps(nextProps: PortalProps): void {
@@ -182,17 +181,17 @@ export class Portal extends React.Component<PortalProps, PortalState> {
prevPathname: nextProps.location.pathname,
});
}
// If the address changed, but the network did not, we can just refetch the currently tracked tokens.
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
(nextProps.userAddress !== this.props.userAddress && nextProps.networkId === this.props.networkId) ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses());
}
const nextTrackedTokens = this._getTrackedTokens(nextProps.tokenByAddress);
const nextTrackedTokens = utils.getTrackedTokens(nextProps.tokenByAddress);
const trackedTokens = this._getCurrentTrackedTokens();
if (!_.isEqual(nextTrackedTokens, trackedTokens)) {
@@ -200,7 +199,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
const newTokenAddresses = _.map(newTokens, token => token.address);
// Add placeholder entry for this token to the state, since fetching the
// balance/allowance is asynchronous
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
const trackedTokenStateByAddress = { ...this.state.trackedTokenStateByAddress };
for (const tokenAddress of newTokenAddresses) {
trackedTokenStateByAddress[tokenAddress] = {
balance: new BigNumber(0),
@@ -265,16 +264,16 @@ export class Portal extends React.Component<PortalProps, PortalState> {
networkId={this.props.networkId}
/>
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
{this.props.blockchainIsLoaded && (
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this._onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
)}
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this._onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
<AssetPicker
userAddress={this.props.userAddress}
networkId={this.props.networkId}
@@ -563,9 +562,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
if (this.state.tokenManagementState === TokenManagementState.Remove && !isDefaultTrackedToken) {
if (token.isRegistered) {
// Remove the token from tracked tokens
const newToken = {
const newToken: Token = {
...token,
isTracked: false,
trackedTimestamp: undefined,
};
this.props.dispatcher.updateTokenByAddress([newToken]);
} else {
@@ -608,17 +607,12 @@ export class Portal extends React.Component<PortalProps, PortalState> {
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
return isSmallScreen;
}
private _getCurrentTrackedTokens(): Token[] {
return this._getTrackedTokens(this.props.tokenByAddress);
return utils.getTrackedTokens(this.props.tokenByAddress);
}
private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] {
const allTokens = _.values(tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return trackedTokens;
private _getCurrentTrackedTokensAddresses(): string[] {
return _.map(this._getCurrentTrackedTokens(), token => token.address);
}
private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress {
const trackedTokenStateByAddress: TokenStateByAddress = {};
_.each(trackedTokens, token => {

View File

@@ -308,7 +308,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
const trackedTokensStartingWithEtherToken = trackedTokens.sort(
firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL)
.thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL)
.thenBy('address'),
.thenBy('trackedTimestamp'),
);
const tableRows = _.map(
trackedTokensStartingWithEtherToken,
@@ -424,9 +424,9 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
if (!this.state.isAddingToken && !isDefaultTrackedToken) {
if (token.isRegistered) {
// Remove the token from tracked tokens
const newToken = {
const newToken: Token = {
...token,
isTracked: false,
trackedTimestamp: undefined,
};
this.props.dispatcher.updateTokenByAddress([newToken]);
} else {

View File

@@ -13,7 +13,6 @@ import { ProviderDisplay } from 'ts/components/top_bar/provider_display';
import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
import { DropDown } from 'ts/components/ui/drop_down';
import { Dispatcher } from 'ts/redux/dispatcher';
import { zIndex } from 'ts/style/z_index';
import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { Translate } from 'ts/utils/translate';
@@ -58,7 +57,6 @@ const styles: Styles = {
width: '100%',
position: 'relative',
top: 0,
zIndex: zIndex.topBar,
paddingBottom: 1,
},
bottomBar: {

View File

@@ -156,6 +156,20 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
isHoveringSidebar: false,
};
}
public componentDidUpdate(prevProps: WalletProps): void {
const currentTrackedTokens = this.props.trackedTokens;
const differentTrackedTokens = _.difference(currentTrackedTokens, prevProps.trackedTokens);
const firstDifferentTrackedToken = _.head(differentTrackedTokens);
// check if there is only one different token, and if that token is a member of the current tracked tokens
// this means that the token was added, not removed
if (
!_.isUndefined(firstDifferentTrackedToken) &&
_.size(differentTrackedTokens) === 1 &&
_.includes(currentTrackedTokens, firstDifferentTrackedToken)
) {
document.getElementById(firstDifferentTrackedToken.address).scrollIntoView();
}
}
public render(): React.ReactNode {
const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
return (
@@ -318,7 +332,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
const trackedTokensStartingWithEtherToken = trackedTokens.sort(
firstBy((t: Token) => t.symbol !== constants.ETHER_TOKEN_SYMBOL)
.thenBy((t: Token) => t.symbol !== constants.ZRX_TOKEN_SYMBOL)
.thenBy('address'),
.thenBy('trackedTimestamp'),
);
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
}
@@ -375,7 +389,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
const style = { ...styles.tokenItem, ...additionalStyle };
const etherToken = this._getEthToken();
return (
<div key={key} className={`flex flex-column ${className || ''}`}>
<div id={key} key={key} className={`flex flex-column ${className || ''}`}>
<StandardIconRow
icon={icon}
main={

View File

@@ -97,6 +97,7 @@ render(
<Route path={WebsitePaths.FAQ} component={FAQ as any} />
<Route path={WebsitePaths.About} component={About as any} />
<Route path={WebsitePaths.Wiki} component={Wiki as any} />
<Route path={`${WebsitePaths.Docs}`} component={LazyZeroExJSDocumentation} />
<Route path={`${WebsitePaths.ZeroExJs}/:version?`} component={LazyZeroExJSDocumentation} />
<Route path={`${WebsitePaths.Connect}/:version?`} component={LazyConnectDocumentation} />
<Route

View File

@@ -11,15 +11,15 @@ export interface NotFoundProps {
dispatcher: Dispatcher;
}
export const NotFound = (_props: NotFoundProps) => {
export const NotFound = (props: NotFoundProps) => {
return (
<div>
<TopBar blockchainIsLoaded={false} location={this.props.location} translate={this.props.translate} />
<TopBar blockchainIsLoaded={false} location={props.location} translate={props.translate} />
<FullscreenMessage
headerText={'404 Not Found'}
bodyText={"Hm... looks like we couldn't find what you are looking for."}
/>
<Footer translate={this.props.translate} dispatcher={this.props.dispatcher} />
<Footer translate={props.translate} dispatcher={props.dispatcher} />
</div>
);
};

View File

@@ -13,8 +13,8 @@ export interface Token {
address: string;
symbol: string;
decimals: number;
isTracked: boolean;
isRegistered: boolean;
trackedTimestamp?: number;
}
export interface TokenByAddress {
@@ -356,6 +356,7 @@ export enum WebsiteLegacyPaths {
export enum WebsitePaths {
Portal = '/portal',
Wiki = '/wiki',
Docs = '/docs',
ZeroExJs = '/docs/0x.js',
Home = '/',
FAQ = '/faq',
@@ -488,6 +489,16 @@ export enum Providers {
Mist = 'MIST',
}
export interface InjectedProviderUpdate {
selectedAddress: string;
networkVersion: string;
}
export interface InjectedProviderObservable {
subscribe(updateHandler: (update: InjectedProviderUpdate) => void): void;
unsubscribe(updateHandler: (update: InjectedProviderUpdate) => void): void;
}
export interface TimestampMsRange {
startTimestampMs: number;
endTimestampMs: number;

View File

@@ -63,10 +63,9 @@ export const configs = {
TKN: '/images/token_icons/tokencard.png',
TRST: '/images/token_icons/trust.png',
} as { [symbol: string]: string },
IS_MAINNET_ENABLED: true,
GOOGLE_ANALYTICS_ID: 'UA-98720122-1',
LAST_LOCAL_STORAGE_FILL_CLEARANCE_DATE: '2017-11-22',
LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2017-12-19',
LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2018-6-25',
OUTDATED_WRAPPED_ETHERS: [
{
42: {

View File

@@ -355,6 +355,11 @@ export const utils = {
const token = _.find(tokens, { symbol });
return token;
},
getTrackedTokens(tokenByAddress: TokenByAddress): Token[] {
const allTokens = _.values(tokenByAddress);
const trackedTokens = _.filter(allTokens, t => this.isTokenTracked(t));
return trackedTokens;
},
getFormattedAmountFromToken(token: Token, tokenState: TokenState): string {
return utils.getFormattedAmount(tokenState.balance, token.decimals, token.symbol);
},
@@ -381,4 +386,7 @@ export const utils = {
return BrowserType.Other;
}
},
isTokenTracked(token: Token): boolean {
return !_.isUndefined(token.trackedTimestamp);
},
};

View File

@@ -44,6 +44,24 @@
ethereumjs-util "5.1.5"
lodash "4.17.10"
"@0xproject/order-watcher@^0.0.6":
version "0.0.6"
resolved "https://registry.npmjs.org/@0xproject/order-watcher/-/order-watcher-0.0.6.tgz#85a8fb21e5755bb555f427b12d64d10b89b332e6"
dependencies:
"@0xproject/assert" "^0.2.12"
"@0xproject/base-contract" "^0.3.4"
"@0xproject/contract-wrappers" "^0.0.5"
"@0xproject/fill-scenarios" "^0.0.4"
"@0xproject/json-schemas" "^0.8.1"
"@0xproject/order-utils" "^0.0.7"
"@0xproject/types" "^0.8.1"
"@0xproject/typescript-typings" "^0.4.1"
"@0xproject/utils" "^0.7.1"
"@0xproject/web3-wrapper" "^0.7.1"
bintrees "1.0.2"
ethers "3.0.22"
lodash "4.17.10"
"@0xproject/types@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@0xproject/types/-/types-0.5.0.tgz#ba3cfbc11a8c6344b57c9680aa7df2ea84b9bf05"
@@ -1708,7 +1726,7 @@ bindings@^1.2.1, bindings@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
bintrees@^1.0.2:
bintrees@1.0.2, bintrees@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8"