diff options
author | fragosti <francesco.agosti93@gmail.com> | 2018-06-29 08:20:05 +0800 |
---|---|---|
committer | fragosti <francesco.agosti93@gmail.com> | 2018-06-29 08:20:05 +0800 |
commit | e235a63119216525815430832ebf88fbef5e9259 (patch) | |
tree | 8b62a0dbc9871988b31db4228e8f18a500212717 /packages | |
parent | c20549e88d710cb5b3577fae468545aa2d531370 (diff) | |
parent | 518a2da0275632a5dd61f99a105163ff5a074927 (diff) | |
download | dexon-0x-contracts-e235a63119216525815430832ebf88fbef5e9259.tar.gz dexon-0x-contracts-e235a63119216525815430832ebf88fbef5e9259.tar.zst dexon-0x-contracts-e235a63119216525815430832ebf88fbef5e9259.zip |
Merge branch 'v2-prototype' of https://github.com/0xProject/0x-monorepo into feature/website/portal-onboarding-polish
Diffstat (limited to 'packages')
48 files changed, 2580 insertions, 1012 deletions
diff --git a/packages/contracts/src/abstract/abstract_asset_wrapper.ts b/packages/contracts/src/abstract/abstract_asset_wrapper.ts new file mode 100644 index 000000000..4b56a8502 --- /dev/null +++ b/packages/contracts/src/abstract/abstract_asset_wrapper.ts @@ -0,0 +1,3 @@ +export abstract class AbstractAssetWrapper { + public abstract getProxyId(): string; +} diff --git a/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol index 5503eb2f2..78ea96447 100644 --- a/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol +++ b/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol @@ -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); + } } diff --git a/packages/contracts/src/utils/asset_wrapper.ts b/packages/contracts/src/utils/asset_wrapper.ts new file mode 100644 index 000000000..a7f91f413 --- /dev/null +++ b/packages/contracts/src/utils/asset_wrapper.ts @@ -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); + } + } +} diff --git a/packages/contracts/src/utils/core_combinatorial_utils.ts b/packages/contracts/src/utils/core_combinatorial_utils.ts new file mode 100644 index 000000000..5b725fbe3 --- /dev/null +++ b/packages/contracts/src/utils/core_combinatorial_utils.ts @@ -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 diff --git a/packages/contracts/src/utils/erc20_wrapper.ts b/packages/contracts/src/utils/erc20_wrapper.ts index 91c9d50b7..7f00e2061 100644 --- a/packages/contracts/src/utils/erc20_wrapper.ts +++ b/packages/contracts/src/utils/erc20_wrapper.ts @@ -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"'); diff --git a/packages/contracts/src/utils/erc721_wrapper.ts b/packages/contracts/src/utils/erc721_wrapper.ts index b20d9acd2..309135ae4 100644 --- a/packages/contracts/src/utils/erc721_wrapper.ts +++ b/packages/contracts/src/utils/erc721_wrapper.ts @@ -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"'); diff --git a/packages/contracts/src/utils/exchange_wrapper.ts b/packages/contracts/src/utils/exchange_wrapper.ts index 06eb70644..ad68c8ff4 100644 --- a/packages/contracts/src/utils/exchange_wrapper.ts +++ b/packages/contracts/src/utils/exchange_wrapper.ts @@ -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; diff --git a/packages/contracts/src/utils/order_factory_from_scenario.ts b/packages/contracts/src/utils/order_factory_from_scenario.ts new file mode 100644 index 000000000..b150e59f6 --- /dev/null +++ b/packages/contracts/src/utils/order_factory_from_scenario.ts @@ -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; + } +} diff --git a/packages/contracts/src/utils/order_utils.ts b/packages/contracts/src/utils/order_utils.ts index a9f994d80..019f6e74b 100644 --- a/packages/contracts/src/utils/order_utils.ts +++ b/packages/contracts/src/utils/order_utils.ts @@ -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), diff --git a/packages/contracts/src/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/packages/contracts/src/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..a295a40c4 --- /dev/null +++ b/packages/contracts/src/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts @@ -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; + } +} diff --git a/packages/contracts/src/utils/simple_order_filled_cancelled_fetcher.ts b/packages/contracts/src/utils/simple_order_filled_cancelled_fetcher.ts new file mode 100644 index 000000000..24afe36b7 --- /dev/null +++ b/packages/contracts/src/utils/simple_order_filled_cancelled_fetcher.ts @@ -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; + } +} diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index 5dfac64fc..b792bb90a 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -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; +} diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts index 595839519..39121d95c 100644 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -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( diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index 36f6fa8d3..0c7381aac 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -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 diff --git a/packages/contracts/test/exchange/dispatcher.ts b/packages/contracts/test/exchange/dispatcher.ts index 8adbee6a3..e35cca845 100644 --- a/packages/contracts/test/exchange/dispatcher.ts +++ b/packages/contracts/test/exchange/dispatcher.ts @@ -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(); diff --git a/packages/contracts/test/exchange/fill_order.ts b/packages/contracts/test/exchange/fill_order.ts new file mode 100644 index 000000000..d65ab2f9a --- /dev/null +++ b/packages/contracts/test/exchange/fill_order.ts @@ -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); + }); + }); +}); diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts index bcf691471..e23ab9851 100644 --- a/packages/contracts/test/exchange/match_orders.ts +++ b/packages/contracts/test/exchange/match_orders.ts @@ -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 diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts index b0c08e46e..c4de58bb9 100644 --- a/packages/contracts/test/exchange/transactions.ts +++ b/packages/contracts/test/exchange/transactions.ts @@ -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(); diff --git a/packages/contracts/test/exchange/wrapper.ts b/packages/contracts/test/exchange/wrapper.ts index 9ee68f94d..1af8bde9d 100644 --- a/packages/contracts/test/exchange/wrapper.ts +++ b/packages/contracts/test/exchange/wrapper.ts @@ -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); diff --git a/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts b/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts index ec398a11e..865ea4e43 100644 --- a/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts +++ b/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts @@ -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; } diff --git a/packages/order-utils/src/exchange_transfer_simulator.ts b/packages/order-utils/src/exchange_transfer_simulator.ts index 32d53d6a2..72ed85406 100644 --- a/packages/order-utils/src/exchange_transfer_simulator.ts +++ b/packages/order-utils/src/exchange_transfer_simulator.ts @@ -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)); } diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 514488e02..b4a7a6b67 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -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'; diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index 40f235da7..dd45a1298 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -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 remainingFillableTakerAssetAmount = sidedOrderRelevantState.remainingFillableAssetAmount + .times(signedOrder.takerAssetAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + + const orderRelevantState = { + 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 makerFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( - zrxTokenAddress, - signedOrder.makerAddress, + 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 makerFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( - zrxTokenAddress, - signedOrder.makerAddress, + 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 isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(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 - .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 remainingMakerAssetAmount = remainingTakerAssetAmount.eq(0) + ? new BigNumber(0) + : remainingTakerAssetAmount.times(totalMakerAssetAmount).dividedToIntegerBy(totalTakerAssetAmount); + const remainingAssetAmount = isMakerSide ? remainingMakerAssetAmount : remainingTakerAssetAmount; + const remainingFillableCalculator = new RemainingFillableCalculator( - signedOrder.makerFee, - signedOrder.makerAssetAmount, - isMakerAssetZRX, - transferrableMakerAssetAmount, + feeAmount, + assetAmount, + isAssetZRX, + transferrableTraderAssetAmount, transferrableFeeAssetAmount, - remainingMakerAssetAmount, + remainingAssetAmount, ); - const remainingFillableMakerAssetAmount = remainingFillableCalculator.computeRemainingFillable(); - const remainingFillableTakerAssetAmount = remainingFillableMakerAssetAmount - .times(signedOrder.takerAssetAmount) - .dividedToIntegerBy(signedOrder.makerAssetAmount); - const orderRelevantState = { - makerBalance, - makerProxyAllowance, - makerFeeBalance, - makerFeeProxyAllowance, + const remainingFillableAssetAmount = remainingFillableCalculator.computeRemainingFillable(); + + const sidedOrderRelevantState = { + isMakerSide, + traderBalance, + traderProxyAllowance, + traderFeeBalance, + traderFeeProxyAllowance, filledTakerAssetAmount, - remainingFillableMakerAssetAmount, - remainingFillableTakerAssetAmount, + remainingFillableAssetAmount, }; - return orderRelevantState; + return sidedOrderRelevantState; } } diff --git a/packages/order-utils/src/order_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts index 3a6704f26..54428f77d 100644 --- a/packages/order-utils/src/order_validation_utils.ts +++ b/packages/order-utils/src/order_validation_utils.ts @@ -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); } } } diff --git a/packages/order-utils/src/remaining_fillable_calculator.ts b/packages/order-utils/src/remaining_fillable_calculator.ts index bc146e931..29e19e5ab 100644 --- a/packages/order-utils/src/remaining_fillable_calculator.ts +++ b/packages/order-utils/src/remaining_fillable_calculator.ts @@ -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()) { diff --git a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts index 08d50b924..e7352119d 100644 --- a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts +++ b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts @@ -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 = {}; } diff --git a/packages/order-utils/src/utils.ts b/packages/order-utils/src/utils.ts index 6149316f6..7aaaf0609 100644 --- a/packages/order-utils/src/utils.ts +++ b/packages/order-utils/src/utils.ts @@ -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; + }, }; diff --git a/packages/order-utils/test/exchange_transfer_simulator_test.ts b/packages/order-utils/test/exchange_transfer_simulator_test.ts index 52385f611..bed04d879 100644 --- a/packages/order-utils/test/exchange_transfer_simulator_test.ts +++ b/packages/order-utils/test/exchange_transfer_simulator_test.ts @@ -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); diff --git a/packages/order-watcher/test/global_hooks.ts b/packages/order-watcher/test/global_hooks.ts index 9d6903af5..30b0cd697 100644 --- a/packages/order-watcher/test/global_hooks.ts +++ b/packages/order-watcher/test/global_hooks.ts @@ -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, diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 46a4d6629..d18c34c32 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -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 { diff --git a/packages/website/ts/blockchain_watcher.ts b/packages/website/ts/blockchain_watcher.ts index c576db6ac..df5f73fd1 100644 --- a/packages/website/ts/blockchain_watcher.ts +++ b/packages/website/ts/blockchain_watcher.ts @@ -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); } } } diff --git a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx index b968a3147..c8e10303f 100644 --- a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx +++ b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx @@ -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> ); diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index c9727b553..38e4732a4 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -282,6 +282,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, if (didSucceed) { this.setState({ stepIndex: LedgerSteps.SELECT_ADDRESS, + connectionErrMsg: '', }); } return didSucceed; diff --git a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx index f6594b9d5..3751ce06f 100644 --- a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx +++ b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx @@ -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]); } diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx index f3ea44286..03ba1183d 100644 --- a/packages/website/ts/components/fill_order.tsx +++ b/packages/website/ts/components/fill_order.tsx @@ -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) { diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx index 0f569e1ad..3d53a9e7d 100644 --- a/packages/website/ts/components/generate_order/asset_picker.tsx +++ b/packages/website/ts/components/generate_order/asset_picker.tsx @@ -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]); diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx index 176a0807b..3d7eda84c 100644 --- a/packages/website/ts/components/generate_order/new_token_form.tsx +++ b/packages/website/ts/components/generate_order/new_token_form.tsx @@ -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); diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index b4a174a03..c85d97207 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -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} diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 6486bafcb..1850a0966 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -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)} diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 4166fde53..438c7b52f 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -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 => { diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 7af80745c..3fae83c00 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -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 { diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 537edc7bb..fac6c131f 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -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: { diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 5dde0f4e7..e1a2d4ce7 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -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={ diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index 249b4fdc9..23387a95a 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -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 diff --git a/packages/website/ts/pages/not_found.tsx b/packages/website/ts/pages/not_found.tsx index 2fe3b1f45..a7992a8fa 100644 --- a/packages/website/ts/pages/not_found.tsx +++ b/packages/website/ts/pages/not_found.tsx @@ -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> ); }; diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 8a88c31b2..3211ddbd0 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -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; diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index ace8a5ba0..2f89f8ccb 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -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: { diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index ff06d923d..d12ae6a98 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -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); + }, }; |