diff options
author | Amir Bandeali <abandeali1@gmail.com> | 2018-05-16 03:52:49 +0800 |
---|---|---|
committer | Amir Bandeali <abandeali1@gmail.com> | 2018-05-16 03:52:49 +0800 |
commit | 9e0471bfbb4bb2b3b490e10ce34b16c88e8bab9a (patch) | |
tree | f72aae5170b6f1f6d3d70ebf6c03ed171680ff50 /packages/contract-wrappers/test | |
parent | 9744b1906a111aa0c65c8fafb4db66aef32a5a23 (diff) | |
parent | 6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b (diff) | |
download | dexon-0x-contracts-9e0471bfbb4bb2b3b490e10ce34b16c88e8bab9a.tar.gz dexon-0x-contracts-9e0471bfbb4bb2b3b490e10ce34b16c88e8bab9a.tar.zst dexon-0x-contracts-9e0471bfbb4bb2b3b490e10ce34b16c88e8bab9a.zip |
Merge branch 'development' into v2-prototype
Diffstat (limited to 'packages/contract-wrappers/test')
14 files changed, 3293 insertions, 0 deletions
diff --git a/packages/contract-wrappers/test/artifacts_test.ts b/packages/contract-wrappers/test/artifacts_test.ts new file mode 100644 index 000000000..5d7261e09 --- /dev/null +++ b/packages/contract-wrappers/test/artifacts_test.ts @@ -0,0 +1,49 @@ +import { web3Factory } from '@0xproject/dev-utils'; +import * as fs from 'fs'; + +import { ContractWrappers } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; + +chaiSetup.configure(); + +// Those tests are slower cause they're talking to a remote node +const TIMEOUT = 10000; + +describe('Artifacts', () => { + describe('contracts are deployed on kovan', () => { + const kovanRpcUrl = constants.KOVAN_RPC_URL; + const provider = web3Factory.create({ rpcUrl: kovanRpcUrl }).currentProvider; + const config = { + networkId: constants.KOVAN_NETWORK_ID, + }; + const contractWrappers = new ContractWrappers(provider, config); + it('token registry contract is deployed', async () => { + await (contractWrappers.tokenRegistry as any)._getTokenRegistryContractAsync(); + }).timeout(TIMEOUT); + it('proxy contract is deployed', async () => { + await (contractWrappers.proxy as any)._getTokenTransferProxyContractAsync(); + }).timeout(TIMEOUT); + it('exchange contract is deployed', async () => { + await (contractWrappers.exchange as any)._getExchangeContractAsync(); + }).timeout(TIMEOUT); + }); + describe('contracts are deployed on ropsten', () => { + const ropstenRpcUrl = constants.ROPSTEN_RPC_URL; + const provider = web3Factory.create({ rpcUrl: ropstenRpcUrl }).currentProvider; + const config = { + networkId: constants.ROPSTEN_NETWORK_ID, + }; + const contractWrappers = new ContractWrappers(provider, config); + it('token registry contract is deployed', async () => { + await (contractWrappers.tokenRegistry as any)._getTokenRegistryContractAsync(); + }).timeout(TIMEOUT); + it('proxy contract is deployed', async () => { + await (contractWrappers.proxy as any)._getTokenTransferProxyContractAsync(); + }).timeout(TIMEOUT); + it('exchange contract is deployed', async () => { + await (contractWrappers.exchange as any)._getExchangeContractAsync(); + }).timeout(TIMEOUT); + }); +}); diff --git a/packages/contract-wrappers/test/ether_token_wrapper_test.ts b/packages/contract-wrappers/test/ether_token_wrapper_test.ts new file mode 100644 index 000000000..e9a9705b1 --- /dev/null +++ b/packages/contract-wrappers/test/ether_token_wrapper_test.ts @@ -0,0 +1,417 @@ +import { BlockchainLifecycle, callbackErrorReporter, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { DoneCallback } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import 'mocha'; + +import { + ApprovalContractEventArgs, + BlockParamLiteral, + BlockRange, + ContractWrappers, + ContractWrappersError, + DecodedLogEvent, + DepositContractEventArgs, + EtherTokenEvents, + Token, + TransferContractEventArgs, + WithdrawalContractEventArgs, +} from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { TokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +// Since the address depositing/withdrawing ETH/WETH also needs to pay gas costs for the transaction, +// a small amount of ETH will be used to pay this gas cost. We therefore check that the difference between +// the expected balance and actual balance (given the amount of ETH deposited), only deviates by the amount +// required to pay gas costs. +const MAX_REASONABLE_GAS_COST_IN_WEI = 62517; + +describe('EtherTokenWrapper', () => { + let contractWrappers: ContractWrappers; + let tokens: Token[]; + let userAddresses: string[]; + let addressWithETH: string; + let wethContractAddress: string; + let depositWeiAmount: BigNumber; + let decimalPlaces: number; + let addressWithoutFunds: string; + const gasPrice = new BigNumber(1); + const zeroExConfig = { + gasPrice, + networkId: constants.TESTRPC_NETWORK_ID, + }; + const transferAmount = new BigNumber(42); + const allowanceAmount = new BigNumber(42); + const depositAmount = new BigNumber(42); + const withdrawalAmount = new BigNumber(42); + before(async () => { + contractWrappers = new ContractWrappers(provider, zeroExConfig); + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + addressWithETH = userAddresses[0]; + wethContractAddress = contractWrappers.etherToken.getContractAddressIfExists() as string; + depositWeiAmount = Web3Wrapper.toWei(new BigNumber(5)); + decimalPlaces = 7; + addressWithoutFunds = userAddresses[1]; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#getContractAddressIfExists', async () => { + it('should return contract address if connected to a known network', () => { + const contractAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists(); + expect(contractAddressIfExists).to.not.be.undefined(); + }); + it('should throw if connected to a private network and contract addresses are not specified', () => { + const UNKNOWN_NETWORK_NETWORK_ID = 10; + expect( + () => + new ContractWrappers(provider, { + networkId: UNKNOWN_NETWORK_NETWORK_ID, + } as any), + ).to.throw(); + }); + }); + describe('#depositAsync', () => { + it('should successfully deposit ETH and issue Wrapped ETH tokens', async () => { + const preETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const preWETHBalance = await contractWrappers.token.getBalanceAsync(wethContractAddress, addressWithETH); + expect(preETHBalance).to.be.bignumber.gt(0); + expect(preWETHBalance).to.be.bignumber.equal(0); + + const txHash = await contractWrappers.etherToken.depositAsync( + wethContractAddress, + depositWeiAmount, + addressWithETH, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + + const postETHBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const postWETHBalanceInBaseUnits = await contractWrappers.token.getBalanceAsync( + wethContractAddress, + addressWithETH, + ); + + expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(depositWeiAmount); + const remainingETHInWei = preETHBalance.minus(depositWeiAmount); + const gasCost = remainingETHInWei.minus(postETHBalanceInWei); + expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI); + }); + it('should throw if user has insufficient ETH balance for deposit', async () => { + const preETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH); + + const extraETHBalance = Web3Wrapper.toWei(new BigNumber(5)); + const overETHBalanceinWei = preETHBalance.add(extraETHBalance); + + return expect( + contractWrappers.etherToken.depositAsync(wethContractAddress, overETHBalanceinWei, addressWithETH), + ).to.be.rejectedWith(ContractWrappersError.InsufficientEthBalanceForDeposit); + }); + }); + describe('#withdrawAsync', () => { + it('should successfully withdraw ETH in return for Wrapped ETH tokens', async () => { + const ETHBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(addressWithETH); + + await contractWrappers.etherToken.depositAsync(wethContractAddress, depositWeiAmount, addressWithETH); + + const expectedPreETHBalance = ETHBalanceInWei.minus(depositWeiAmount); + const preETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const preWETHBalance = await contractWrappers.token.getBalanceAsync(wethContractAddress, addressWithETH); + let gasCost = expectedPreETHBalance.minus(preETHBalance); + expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI); + expect(preWETHBalance).to.be.bignumber.equal(depositWeiAmount); + + const txHash = await contractWrappers.etherToken.withdrawAsync( + wethContractAddress, + depositWeiAmount, + addressWithETH, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + + const postETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const postWETHBalanceInBaseUnits = await contractWrappers.token.getBalanceAsync( + wethContractAddress, + addressWithETH, + ); + + expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(0); + const expectedETHBalance = preETHBalance.add(depositWeiAmount).round(decimalPlaces); + gasCost = expectedETHBalance.minus(postETHBalance); + expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI); + }); + it('should throw if user has insufficient WETH balance for withdrawal', async () => { + const preWETHBalance = await contractWrappers.token.getBalanceAsync(wethContractAddress, addressWithETH); + expect(preWETHBalance).to.be.bignumber.equal(0); + + const overWETHBalance = preWETHBalance.add(999999999); + + return expect( + contractWrappers.etherToken.withdrawAsync(wethContractAddress, overWETHBalance, addressWithETH), + ).to.be.rejectedWith(ContractWrappersError.InsufficientWEthBalanceForWithdrawal); + }); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + let etherTokenAddress: string; + before(() => { + const tokenUtils = new TokenUtils(tokens); + const etherToken = tokenUtils.getWethTokenOrThrow(); + etherTokenAddress = etherToken.address; + }); + afterEach(() => { + contractWrappers.etherToken.unsubscribeAll(); + }); + // Hack: Mocha does not allow a test to be both async and have a `done` callback + // Since we need to await the receipt of the event in the `subscribe` callback, + // we do need both. A hack is to make the top-level async fn w/ a done callback and then + // wrap the rest of the test in an async block + // Source: https://github.com/mochajs/mocha/issues/2407 + it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<TransferContractEventArgs>) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + expect(logEvent.log.logIndex).to.be.equal(0); + expect(logEvent.log.transactionIndex).to.be.equal(0); + expect(logEvent.log.blockNumber).to.be.a('number'); + const args = logEvent.log.args; + expect(args._from).to.be.equal(addressWithETH); + expect(args._to).to.be.equal(addressWithoutFunds); + expect(args._value).to.be.bignumber.equal(transferAmount); + }, + ); + await contractWrappers.etherToken.depositAsync(etherTokenAddress, transferAmount, addressWithETH); + contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Transfer, + indexFilterValues, + callback, + ); + await contractWrappers.token.transferAsync( + etherTokenAddress, + addressWithETH, + addressWithoutFunds, + transferAmount, + ); + })().catch(done); + }); + it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; + expect(args._owner).to.be.equal(addressWithETH); + expect(args._spender).to.be.equal(addressWithoutFunds); + expect(args._value).to.be.bignumber.equal(allowanceAmount); + }, + ); + contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Approval, + indexFilterValues, + callback, + ); + await contractWrappers.token.setAllowanceAsync( + etherTokenAddress, + addressWithETH, + addressWithoutFunds, + allowanceAmount, + ); + })().catch(done); + }); + it('Should receive the Deposit event when ether is being deposited', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<DepositContractEventArgs>) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; + expect(args._owner).to.be.equal(addressWithETH); + expect(args._value).to.be.bignumber.equal(depositAmount); + }, + ); + contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Deposit, + indexFilterValues, + callback, + ); + await contractWrappers.etherToken.depositAsync(etherTokenAddress, depositAmount, addressWithETH); + })().catch(done); + }); + it('Should receive the Withdrawal event when ether is being withdrawn', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<WithdrawalContractEventArgs>) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; + expect(args._owner).to.be.equal(addressWithETH); + expect(args._value).to.be.bignumber.equal(depositAmount); + }, + ); + await contractWrappers.etherToken.depositAsync(etherTokenAddress, depositAmount, addressWithETH); + contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Withdrawal, + indexFilterValues, + callback, + ); + await contractWrappers.etherToken.withdrawAsync(etherTokenAddress, withdrawalAmount, addressWithETH); + })().catch(done); + }); + it('should cancel outstanding subscriptions when ZeroEx.setProvider is called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(); + contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); + await contractWrappers.etherToken.depositAsync(etherTokenAddress, transferAmount, addressWithETH); + contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Transfer, + indexFilterValues, + callbackToBeCalled, + ); + await contractWrappers.token.transferAsync( + etherTokenAddress, + addressWithETH, + addressWithoutFunds, + transferAmount, + ); + })().catch(done); + }); + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + await contractWrappers.etherToken.depositAsync(etherTokenAddress, transferAmount, addressWithETH); + const subscriptionToken = contractWrappers.etherToken.subscribe( + etherTokenAddress, + EtherTokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + contractWrappers.etherToken.unsubscribe(subscriptionToken); + await contractWrappers.token.transferAsync( + etherTokenAddress, + addressWithETH, + addressWithoutFunds, + transferAmount, + ); + done(); + })().catch(done); + }); + }); + describe('#getLogsAsync', () => { + let etherTokenAddress: string; + let tokenTransferProxyAddress: string; + const blockRange: BlockRange = { + fromBlock: 0, + toBlock: BlockParamLiteral.Latest, + }; + let txHash: string; + before(() => { + addressWithETH = userAddresses[0]; + const tokenUtils = new TokenUtils(tokens); + const etherToken = tokenUtils.getWethTokenOrThrow(); + etherTokenAddress = etherToken.address; + tokenTransferProxyAddress = contractWrappers.proxy.getContractAddress(); + }); + it('should get logs with decoded args emitted by Approval', async () => { + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(etherTokenAddress, addressWithETH); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const eventName = EtherTokenEvents.Approval; + const indexFilterValues = {}; + const logs = await contractWrappers.etherToken.getLogsAsync<ApprovalContractEventArgs>( + etherTokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(logs[0].event).to.be.equal(eventName); + expect(args._owner).to.be.equal(addressWithETH); + expect(args._spender).to.be.equal(tokenTransferProxyAddress); + expect(args._value).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + }); + it('should get logs with decoded args emitted by Deposit', async () => { + await contractWrappers.etherToken.depositAsync(etherTokenAddress, depositAmount, addressWithETH); + const eventName = EtherTokenEvents.Deposit; + const indexFilterValues = {}; + const logs = await contractWrappers.etherToken.getLogsAsync<DepositContractEventArgs>( + etherTokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(logs[0].event).to.be.equal(eventName); + expect(args._owner).to.be.equal(addressWithETH); + expect(args._value).to.be.bignumber.equal(depositAmount); + }); + it('should only get the logs with the correct event name', async () => { + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(etherTokenAddress, addressWithETH); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const differentEventName = EtherTokenEvents.Transfer; + const indexFilterValues = {}; + const logs = await contractWrappers.etherToken.getLogsAsync( + etherTokenAddress, + differentEventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(0); + }); + it('should only get the logs with the correct indexed fields', async () => { + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(etherTokenAddress, addressWithETH); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync( + etherTokenAddress, + addressWithoutFunds, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const eventName = EtherTokenEvents.Approval; + const indexFilterValues = { + _owner: addressWithETH, + }; + const logs = await contractWrappers.etherToken.getLogsAsync<ApprovalContractEventArgs>( + etherTokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(args._owner).to.be.equal(addressWithETH); + }); + }); +}); diff --git a/packages/contract-wrappers/test/exchange_transfer_simulator_test.ts b/packages/contract-wrappers/test/exchange_transfer_simulator_test.ts new file mode 100644 index 000000000..b4ea91181 --- /dev/null +++ b/packages/contract-wrappers/test/exchange_transfer_simulator_test.ts @@ -0,0 +1,119 @@ +import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils'; +import { BlockParamLiteral, Token } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { ContractWrappers, ExchangeContractErrs } from '../src'; +import { TradeSide, TransferType } from '../src/types'; +import { ExchangeTransferSimulator } from '../src/utils/exchange_transfer_simulator'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ExchangeTransferSimulator', () => { + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + const contractWrappers = new ContractWrappers(provider, config); + const transferAmount = new BigNumber(5); + let userAddresses: string[]; + let tokens: Token[]; + let coinbase: string; + let sender: string; + let recipient: string; + let exampleTokenAddress: string; + let exchangeTransferSimulator: ExchangeTransferSimulator; + let txHash: string; + before(async () => { + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + [coinbase, sender, recipient] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + exampleTokenAddress = tokens[0].address; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#transferFromAsync', () => { + beforeEach(() => { + exchangeTransferSimulator = new ExchangeTransferSimulator(contractWrappers.token, BlockParamLiteral.Latest); + }); + it("throws if the user doesn't have enough allowance", async () => { + return expect( + exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Taker, + TransferType.Trade, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance); + }); + it("throws if the user doesn't have enough balance", async () => { + txHash = await contractWrappers.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + return expect( + exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Maker, + TransferType.Trade, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); + }); + it('updates balances and proxyAllowance after transfer', async () => { + txHash = await contractWrappers.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + txHash = await contractWrappers.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + await exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Taker, + 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); + expect(senderBalance).to.be.bignumber.equal(0); + expect(recipientBalance).to.be.bignumber.equal(transferAmount); + expect(senderProxyAllowance).to.be.bignumber.equal(0); + }); + it("doesn't update proxyAllowance after transfer if unlimited", async () => { + txHash = await contractWrappers.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(exampleTokenAddress, sender); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + await exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Taker, + 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); + expect(senderBalance).to.be.bignumber.equal(0); + expect(recipientBalance).to.be.bignumber.equal(transferAmount); + expect(senderProxyAllowance).to.be.bignumber.equal( + contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + }); + }); +}); diff --git a/packages/contract-wrappers/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts new file mode 100644 index 000000000..fc0a23485 --- /dev/null +++ b/packages/contract-wrappers/test/exchange_wrapper_test.ts @@ -0,0 +1,1228 @@ +import { BlockchainLifecycle, callbackErrorReporter, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { FillScenarios } from '@0xproject/fill-scenarios'; +import { getOrderHashHex } from '@0xproject/order-utils'; +import { BlockParamLiteral, DoneCallback, OrderState } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { + BlockRange, + ContractWrappers, + DecodedLogEvent, + ExchangeContractErrs, + ExchangeEvents, + LogCancelContractEventArgs, + LogFillContractEventArgs, + OrderCancellationRequest, + OrderFillRequest, + SignedOrder, + Token, +} from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { TokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777'; + +describe('ExchangeWrapper', () => { + let contractWrappers: ContractWrappers; + let tokenUtils: TokenUtils; + let tokens: Token[]; + let userAddresses: string[]; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let exchangeContractAddress: string; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + exchangeContractAddress = contractWrappers.exchange.getContractAddress(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(provider, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + await fillScenarios.initTokenBalancesAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('fillOrKill order(s)', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const takerTokenFillAmount = new BigNumber(5); + before(async () => { + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + describe('#batchFillOrKillAsync', () => { + it('successfully batch fillOrKill', async () => { + const fillableAmount = new BigNumber(5); + const partialFillTakerAmount = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const orderFillRequests = [ + { + signedOrder, + takerTokenFillAmount: partialFillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: partialFillTakerAmount, + }, + ]; + await contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress); + }); + describe('order transaction options', () => { + let signedOrder: SignedOrder; + let orderFillRequests: OrderFillRequest[]; + const fillableAmount = new BigNumber(5); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + orderFillRequests = [ + { + signedOrder, + takerTokenFillAmount: new BigNumber(0), + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + }); + describe('#fillOrKillOrderAsync', () => { + let signedOrder: SignedOrder; + const fillableAmount = new BigNumber(5); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + describe('successful fills', () => { + it('should fill a valid order', async () => { + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount); + await contractWrappers.exchange.fillOrKillOrderAsync( + signedOrder, + takerTokenFillAmount, + takerAddress, + ); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + }); + it('should partially fill a valid order', async () => { + const partialFillAmount = new BigNumber(3); + await contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + }); + }); + describe('order transaction options', () => { + const emptyFillableAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + }); + }); + describe('fill order(s)', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const fillableAmount = new BigNumber(5); + const takerTokenFillAmount = new BigNumber(5); + const shouldThrowOnInsufficientBalanceOrAllowance = true; + before(async () => { + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + describe('#fillOrderAsync', () => { + describe('successful fills', () => { + it('should fill a valid order', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(0); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount); + const txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(takerTokenFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount)); + }); + it('should partially fill the valid order', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const partialFillAmount = new BigNumber(3); + const txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + partialFillAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress), + ).to.be.bignumber.equal(partialFillAmount); + expect( + await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress), + ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount)); + }); + it('should fill the valid orders with fees', async () => { + const makerFee = new BigNumber(1); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + takerTokenAddress, + makerFee, + takerFee, + makerAddress, + takerAddress, + fillableAmount, + feeRecipient, + ); + const txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + expect( + await contractWrappers.token.getBalanceAsync(zrxTokenAddress, feeRecipient), + ).to.be.bignumber.equal(makerFee.plus(takerFee)); + }); + }); + describe('order transaction options', () => { + let signedOrder: SignedOrder; + const emptyFillTakerAmount = new BigNumber(0); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + emptyFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + emptyFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: true, + }, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + emptyFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: false, + }, + ), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + describe('negative fill amount', async () => { + let signedOrder: SignedOrder; + const negativeFillTakerAmount = new BigNumber(-100); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + it('should not allow the exchange wrapper to fill if amount is negative', async () => { + return expect( + contractWrappers.exchange.fillOrderAsync( + signedOrder, + negativeFillTakerAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejected(); + }); + }); + }); + describe('#batchFillOrdersAsync', () => { + let signedOrder: SignedOrder; + let signedOrderHashHex: string; + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let orderFillBatch: OrderFillRequest[]; + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + signedOrderHashHex = getOrderHashHex(signedOrder); + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + anotherOrderHashHex = getOrderHashHex(anotherSignedOrder); + }); + describe('successful batch fills', () => { + beforeEach(() => { + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount, + }, + ]; + }); + it('should throw if a batch is empty', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + [], + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + }); + it('should successfully fill multiple orders', async () => { + const txHash = await contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(filledAmount).to.be.bignumber.equal(takerTokenFillAmount); + expect(anotherFilledAmount).to.be.bignumber.equal(takerTokenFillAmount); + }); + }); + describe('order transaction options', () => { + beforeEach(async () => { + const emptyFillTakerAmount = new BigNumber(0); + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount: emptyFillTakerAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: emptyFillTakerAmount, + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: true, + }, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: false, + }, + ), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + describe('negative batch fill amount', async () => { + beforeEach(async () => { + const negativeFillTakerAmount = new BigNumber(-100); + orderFillBatch = [ + { + signedOrder, + takerTokenFillAmount, + }, + { + signedOrder: anotherSignedOrder, + takerTokenFillAmount: negativeFillTakerAmount, + }, + ]; + }); + it('should not allow the exchange wrapper to batch fill if any amount is negative', async () => { + return expect( + contractWrappers.exchange.batchFillOrdersAsync( + orderFillBatch, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejected(); + }); + }); + }); + describe('#fillOrdersUpTo', () => { + let signedOrder: SignedOrder; + let signedOrderHashHex: string; + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let signedOrders: SignedOrder[]; + const fillUpToAmount = fillableAmount.plus(fillableAmount).minus(1); + beforeEach(async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + signedOrderHashHex = getOrderHashHex(signedOrder); + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + anotherOrderHashHex = getOrderHashHex(anotherSignedOrder); + signedOrders = [signedOrder, anotherSignedOrder]; + }); + describe('successful batch fills', () => { + it('should throw if a batch is empty', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + [], + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + }); + it('should successfully fill up to specified amount when all orders are fully funded', async () => { + const txHash = await contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + const remainingFillAmount = fillableAmount.minus(1); + expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); + }); + it('should successfully fill up to specified amount and leave the rest of the orders untouched', async () => { + const txHash = await contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const zeroAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(anotherOrderHashHex); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + expect(zeroAmount).to.be.bignumber.equal(0); + }); + it('should successfully fill up to specified amount even if filling all orders would fail', async () => { + const missingBalance = new BigNumber(1); // User will still have enough balance to fill up to 9, + // but won't have 10 to fully fill all orders in a batch. + await contractWrappers.token.transferAsync( + makerTokenAddress, + makerAddress, + coinbase, + missingBalance, + ); + const txHash = await contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + const remainingFillAmount = fillableAmount.minus(1); + expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); + }); + }); + describe('failed batch fills', () => { + it("should fail validation if user doesn't have enough balance without fill up to", async () => { + const missingBalance = new BigNumber(2); // User will only have enough balance to fill up to 8 + await contractWrappers.token.transferAsync( + makerTokenAddress, + makerAddress, + coinbase, + missingBalance, + ); + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + fillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); + }); + }); + describe('order transaction options', () => { + const emptyFillUpToAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + emptyFillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + emptyFillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: true, + }, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.fillOrdersUpToAsync( + signedOrders, + emptyFillUpToAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + { + shouldValidate: false, + }, + ), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + }); + }); + }); + describe('cancel order(s)', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + const fillableAmount = new BigNumber(5); + let signedOrder: SignedOrder; + let orderHashHex: string; + const cancelAmount = new BigNumber(3); + beforeEach(async () => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + orderHashHex = getOrderHashHex(signedOrder); + }); + describe('#cancelOrderAsync', () => { + describe('successful cancels', () => { + it('should cancel an order', async () => { + const txHash = await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmount); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const cancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHashHex); + expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); + }); + }); + describe('order transaction options', () => { + const emptyCancelTakerTokenAmount = new BigNumber(0); + it('should validate when orderTransactionOptions are not present', async () => { + return expect( + contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + }); + }); + describe('#batchCancelOrdersAsync', () => { + let anotherSignedOrder: SignedOrder; + let anotherOrderHashHex: string; + let cancelBatch: OrderCancellationRequest[]; + beforeEach(async () => { + anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + anotherOrderHashHex = getOrderHashHex(anotherSignedOrder); + cancelBatch = [ + { + order: signedOrder, + takerTokenCancelAmount: cancelAmount, + }, + { + order: anotherSignedOrder, + takerTokenCancelAmount: cancelAmount, + }, + ]; + }); + describe('failed batch cancels', () => { + it('should throw when orders have different makers', async () => { + const signedOrderWithDifferentMaker = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + takerAddress, + takerAddress, + fillableAmount, + ); + return expect( + contractWrappers.exchange.batchCancelOrdersAsync([ + cancelBatch[0], + { + order: signedOrderWithDifferentMaker, + takerTokenCancelAmount: cancelAmount, + }, + ]), + ).to.be.rejectedWith(ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed); + }); + }); + describe('successful batch cancels', () => { + it('should cancel a batch of orders', async () => { + await contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch); + const cancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHashHex); + const anotherCancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync( + anotherOrderHashHex, + ); + expect(cancelledAmount).to.be.bignumber.equal(cancelAmount); + expect(anotherCancelledAmount).to.be.bignumber.equal(cancelAmount); + }); + }); + describe('order transaction options', () => { + beforeEach(async () => { + const emptyTakerTokenCancelAmount = new BigNumber(0); + cancelBatch = [ + { + order: signedOrder, + takerTokenCancelAmount: emptyTakerTokenCancelAmount, + }, + { + order: anotherSignedOrder, + takerTokenCancelAmount: emptyTakerTokenCancelAmount, + }, + ]; + }); + it('should validate when orderTransactionOptions are not present', async () => { + return expect(contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch)).to.be.rejectedWith( + ExchangeContractErrs.OrderCancelAmountZero, + ); + }); + it('should validate when orderTransactionOptions specify to validate', async () => { + return expect( + contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch, { + shouldValidate: true, + }), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should not validate when orderTransactionOptions specify not to validate', async () => { + return expect( + contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch, { + shouldValidate: false, + }), + ).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + }); + }); + }); + describe('tests that require partially filled order', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let takerAddress: string; + let fillableAmount: BigNumber; + let partialFillAmount: BigNumber; + let signedOrder: SignedOrder; + let orderHash: string; + before(() => { + takerAddress = userAddresses[1]; + tokenUtils = new TokenUtils(tokens); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + fillableAmount = new BigNumber(5); + partialFillAmount = new BigNumber(2); + signedOrder = await fillScenarios.createPartiallyFilledSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + takerAddress, + fillableAmount, + partialFillAmount, + ); + orderHash = getOrderHashHex(signedOrder); + }); + describe('#getUnavailableTakerAmountAsync', () => { + it('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + return expect( + contractWrappers.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex), + ).to.be.rejected(); + }); + it('should return zero if passed a valid but non-existent orderHash', async () => { + const unavailableValueT = await contractWrappers.exchange.getUnavailableTakerAmountAsync( + NON_EXISTENT_ORDER_HASH, + ); + expect(unavailableValueT).to.be.bignumber.equal(0); + }); + it('should return the unavailableValueT for a valid and partially filled orderHash', async () => { + const unavailableValueT = await contractWrappers.exchange.getUnavailableTakerAmountAsync(orderHash); + expect(unavailableValueT).to.be.bignumber.equal(partialFillAmount); + }); + }); + describe('#getFilledTakerAmountAsync', () => { + it('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + return expect( + contractWrappers.exchange.getFilledTakerAmountAsync(invalidOrderHashHex), + ).to.be.rejected(); + }); + it('should return zero if passed a valid but non-existent orderHash', async () => { + const filledValueT = await contractWrappers.exchange.getFilledTakerAmountAsync(NON_EXISTENT_ORDER_HASH); + expect(filledValueT).to.be.bignumber.equal(0); + }); + it('should return the filledValueT for a valid and partially filled orderHash', async () => { + const filledValueT = await contractWrappers.exchange.getFilledTakerAmountAsync(orderHash); + expect(filledValueT).to.be.bignumber.equal(partialFillAmount); + }); + }); + describe('#getCancelledTakerAmountAsync', () => { + it('should throw if passed an invalid orderHash', async () => { + const invalidOrderHashHex = '0x123'; + return expect( + contractWrappers.exchange.getCancelledTakerAmountAsync(invalidOrderHashHex), + ).to.be.rejected(); + }); + it('should return zero if passed a valid but non-existent orderHash', async () => { + const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync( + NON_EXISTENT_ORDER_HASH, + ); + expect(cancelledValueT).to.be.bignumber.equal(0); + }); + it('should return the cancelledValueT for a valid and partially filled orderHash', async () => { + const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHash); + expect(cancelledValueT).to.be.bignumber.equal(0); + }); + it('should return the cancelledValueT for a valid and cancelled orderHash', async () => { + const cancelAmount = fillableAmount.minus(partialFillAmount); + await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmount); + const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHash); + expect(cancelledValueT).to.be.bignumber.equal(cancelAmount); + }); + }); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + const shouldThrowOnInsufficientBalanceOrAllowance = true; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let takerAddress: string; + let makerAddress: string; + let fillableAmount: BigNumber; + let signedOrder: SignedOrder; + const takerTokenFillAmountInBaseUnits = new BigNumber(1); + const cancelTakerAmountInBaseUnits = new BigNumber(1); + before(() => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + fillableAmount = new BigNumber(5); + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + afterEach(async () => { + contractWrappers.exchange.unsubscribeAll(); + }); + // Hack: Mocha does not allow a test to be both async and have a `done` callback + // Since we need to await the receipt of the event in the `subscribe` callback, + // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then + // wrap the rest of the test in an async block + // Source: https://github.com/mochajs/mocha/issues/2407 + it('Should receive the LogFill event when an order is filled', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callback); + await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmountInBaseUnits, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + })().catch(done); + }); + it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => { + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogCancel); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogCancel, indexFilterValues, callback); + await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits); + })().catch(done); + }); + it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled); + + contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); + + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill); + }, + ); + contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callback); + await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmountInBaseUnits, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + })().catch(done); + }); + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + const subscriptionToken = contractWrappers.exchange.subscribe( + ExchangeEvents.LogFill, + indexFilterValues, + callbackNeverToBeCalled, + ); + contractWrappers.exchange.unsubscribe(subscriptionToken); + await contractWrappers.exchange.fillOrderAsync( + signedOrder, + takerTokenFillAmountInBaseUnits, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + done(); + })().catch(done); + }); + }); + describe('#getOrderHashHexUsingContractCallAsync', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let makerAddress: string; + let takerAddress: string; + const fillableAmount = new BigNumber(5); + before(async () => { + [, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + it("get's the same hash as the local function", async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const orderHash = getOrderHashHex(signedOrder); + const orderHashFromContract = await (contractWrappers.exchange as any)._getOrderHashHexUsingContractCallAsync( + signedOrder, + ); + expect(orderHash).to.equal(orderHashFromContract); + }); + }); + describe('#getZRXTokenAddressAsync', () => { + it('gets the same token as is in token registry', () => { + const zrxAddress = contractWrappers.exchange.getZRXTokenAddress(); + const zrxToken = tokenUtils.getProtocolTokenOrThrow(); + expect(zrxAddress).to.equal(zrxToken.address); + }); + }); + describe('#getLogsAsync', () => { + let makerTokenAddress: string; + let takerTokenAddress: string; + let makerAddress: string; + let takerAddress: string; + const fillableAmount = new BigNumber(5); + const shouldThrowOnInsufficientBalanceOrAllowance = true; + const blockRange: BlockRange = { + fromBlock: 0, + toBlock: BlockParamLiteral.Latest, + }; + let txHash: string; + before(async () => { + [, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + it('should get logs with decoded args emitted by LogFill', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const eventName = ExchangeEvents.LogFill; + const indexFilterValues = {}; + const logs = await contractWrappers.exchange.getLogsAsync(eventName, blockRange, indexFilterValues); + expect(logs).to.have.length(1); + expect(logs[0].event).to.be.equal(eventName); + }); + it('should only get the logs with the correct event name', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const differentEventName = ExchangeEvents.LogCancel; + const indexFilterValues = {}; + const logs = await contractWrappers.exchange.getLogsAsync( + differentEventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(0); + }); + it('should only get the logs with the correct indexed fields', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + signedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + + const differentMakerAddress = userAddresses[2]; + const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + differentMakerAddress, + takerAddress, + fillableAmount, + ); + txHash = await contractWrappers.exchange.fillOrderAsync( + anotherSignedOrder, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + + const eventName = ExchangeEvents.LogFill; + const indexFilterValues = { + maker: differentMakerAddress, + }; + const logs = await contractWrappers.exchange.getLogsAsync<LogFillContractEventArgs>( + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(args.maker).to.be.equal(differentMakerAddress); + }); + }); + describe('#getOrderStateAsync', () => { + let maker: string; + let taker: string; + let makerToken: Token; + let takerToken: Token; + let signedOrder: SignedOrder; + let orderState: OrderState; + const fillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), constants.ZRX_DECIMALS); + before(async () => { + [, maker, taker] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + [makerToken, takerToken] = tokenUtils.getDummyTokens(); + }); + it('should report orderStateValid when order is fillable', async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, + takerToken.address, + maker, + taker, + fillableAmount, + ); + orderState = await contractWrappers.exchange.getOrderStateAsync(signedOrder); + expect(orderState.isValid).to.be.true(); + }); + it('should report orderStateInvalid when maker allowance set to 0', async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, + takerToken.address, + maker, + taker, + fillableAmount, + ); + await contractWrappers.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); + orderState = await contractWrappers.exchange.getOrderStateAsync(signedOrder); + expect(orderState.isValid).to.be.false(); + }); + }); +}); // tslint:disable:max-file-line-count diff --git a/packages/contract-wrappers/test/global_hooks.ts b/packages/contract-wrappers/test/global_hooks.ts new file mode 100644 index 000000000..88f202761 --- /dev/null +++ b/packages/contract-wrappers/test/global_hooks.ts @@ -0,0 +1,18 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { runMigrationsAsync } from '@0xproject/migrations'; +import * as path from 'path'; + +import { constants } from './utils/constants'; +import { provider } from './utils/web3_wrapper'; + +before('migrate contracts', async function() { + // HACK: Since the migrations take longer then our global mocha timeout limit + // we manually increase it for this before hook. + this.timeout(20000); + const txDefaults = { + gas: devConstants.GAS_ESTIMATE, + from: devConstants.TESTRPC_FIRST_ADDRESS, + }; + const artifactsDir = `../migrations/artifacts/1.0.0`; + await runMigrationsAsync(provider, artifactsDir, txDefaults); +}); diff --git a/packages/contract-wrappers/test/order_validation_test.ts b/packages/contract-wrappers/test/order_validation_test.ts new file mode 100644 index 000000000..d28549ba2 --- /dev/null +++ b/packages/contract-wrappers/test/order_validation_test.ts @@ -0,0 +1,527 @@ +import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils'; +import { FillScenarios } from '@0xproject/fill-scenarios'; +import { OrderError } from '@0xproject/order-utils'; +import { BlockParamLiteral } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as Sinon from 'sinon'; + +import { ContractWrappers, ContractWrappersError, ExchangeContractErrs, SignedOrder, Token } from '../src'; +import { TradeSide, TransferType } from '../src/types'; +import { ExchangeTransferSimulator } from '../src/utils/exchange_transfer_simulator'; +import { OrderValidationUtils } from '../src/utils/order_validation_utils'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { TokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('OrderValidation', () => { + let contractWrappers: ContractWrappers; + let userAddresses: string[]; + let tokens: Token[]; + let tokenUtils: TokenUtils; + let exchangeContractAddress: string; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const fillableAmount = new BigNumber(5); + const fillTakerAmount = new BigNumber(5); + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + exchangeContractAddress = contractWrappers.exchange.getContractAddress(); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(provider, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('validateOrderFillableOrThrowAsync', () => { + it('should succeed if the order is fillable', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder); + }); + it('should succeed if the maker is buying ZRX and has no ZRX balance', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + zrxTokenAddress, + makerFee, + takerFee, + makerAddress, + takerAddress, + fillableAmount, + feeRecipient, + ); + const zrxMakerBalance = await contractWrappers.token.getBalanceAsync(zrxTokenAddress, makerAddress); + await contractWrappers.token.transferAsync(zrxTokenAddress, makerAddress, takerAddress, zrxMakerBalance); + await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder); + }); + it('should succeed if the maker is buying ZRX and has no ZRX balance and there is no specified taker', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + zrxTokenAddress, + makerFee, + takerFee, + makerAddress, + constants.NULL_ADDRESS, + fillableAmount, + feeRecipient, + ); + const zrxMakerBalance = await contractWrappers.token.getBalanceAsync(zrxTokenAddress, makerAddress); + await contractWrappers.token.transferAsync(zrxTokenAddress, makerAddress, takerAddress, zrxMakerBalance); + await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder); + }); + it('should succeed if the order is asymmetric and fillable', async () => { + const makerFillableAmount = fillableAmount; + const takerFillableAmount = fillableAmount.minus(4); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + makerFillableAmount, + takerFillableAmount, + ); + await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder); + }); + it('should throw when the order is fully filled or cancelled', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + await contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder)).to.be.rejectedWith( + ExchangeContractErrs.OrderRemainingFillAmountZero, + ); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + expirationInPast, + ); + return expect(contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder)).to.be.rejectedWith( + ExchangeContractErrs.OrderFillExpired, + ); + }); + }); + describe('validateFillOrderAndThrowIfInvalidAsync', () => { + it('should throw when the fill amount is zero', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const zeroFillAmount = new BigNumber(0); + return expect( + contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + zeroFillAmount, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should throw when the signature is invalid', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + // 27 <--> 28 + signedOrder.ecSignature.v = 28 - signedOrder.ecSignature.v + 27; + return expect( + contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + fillableAmount, + takerAddress, + ), + ).to.be.rejectedWith(OrderError.InvalidSignature); + }); + it('should throw when the order is fully filled or cancelled', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + await contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect( + contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + fillableAmount, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); + }); + it('should throw when sender is not a taker', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const nonTakerAddress = userAddresses[6]; + return expect( + contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + fillTakerAmount, + nonTakerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + expirationInPast, + ); + return expect( + contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + fillTakerAmount, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); + }); + it('should throw when there a rounding error would have occurred', async () => { + const makerAmount = new BigNumber(3); + const takerAmount = new BigNumber(5); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + makerAmount, + takerAmount, + ); + const fillTakerAmountThatCausesRoundingError = new BigNumber(3); + return expect( + contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + fillTakerAmountThatCausesRoundingError, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.OrderFillRoundingError); + }); + }); + describe('#validateFillOrKillOrderAndThrowIfInvalidAsync', () => { + it('should throw if remaining fillAmount is less then the desired fillAmount', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + const tooLargeFillAmount = new BigNumber(7); + const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount); + await contractWrappers.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference); + await contractWrappers.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount); + await contractWrappers.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference); + await contractWrappers.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount); + + return expect( + contractWrappers.exchange.validateFillOrKillOrderThrowIfInvalidAsync( + signedOrder, + tooLargeFillAmount, + takerAddress, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount); + }); + }); + describe('validateCancelOrderAndThrowIfInvalidAsync', () => { + let signedOrder: SignedOrder; + const cancelAmount = new BigNumber(3); + beforeEach(async () => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getDummyTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + ); + }); + it('should throw when cancel amount is zero', async () => { + const zeroCancelAmount = new BigNumber(0); + return expect( + contractWrappers.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, zeroCancelAmount), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + fillableAmount, + expirationInPast, + ); + return expect( + contractWrappers.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount), + ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired); + }); + it('should throw when order is already cancelled or filled', async () => { + await contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect( + contractWrappers.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, fillableAmount), + ).to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + }); + }); + describe('#validateFillOrderBalancesAllowancesThrowIfInvalidAsync', () => { + let exchangeTransferSimulator: ExchangeTransferSimulator; + let transferFromAsync: Sinon.SinonSpy; + const bigNumberMatch = (expected: BigNumber) => { + return Sinon.match((value: BigNumber) => value.eq(expected)); + }; + beforeEach('create exchangeTransferSimulator', async () => { + exchangeTransferSimulator = new ExchangeTransferSimulator(contractWrappers.token, BlockParamLiteral.Latest); + transferFromAsync = Sinon.spy(); + exchangeTransferSimulator.transferFromAsync = transferFromAsync as any; + }); + it('should call exchangeTransferSimulator.transferFrom in a correct order', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + takerTokenAddress, + makerFee, + takerFee, + makerAddress, + takerAddress, + fillableAmount, + feeRecipient, + ); + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, + signedOrder, + fillableAmount, + takerAddress, + zrxTokenAddress, + ); + expect(transferFromAsync.callCount).to.be.equal(4); + expect( + transferFromAsync + .getCall(0) + .calledWith( + makerTokenAddress, + makerAddress, + takerAddress, + bigNumberMatch(fillableAmount), + TradeSide.Maker, + TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync + .getCall(1) + .calledWith( + takerTokenAddress, + takerAddress, + makerAddress, + bigNumberMatch(fillableAmount), + TradeSide.Taker, + TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync + .getCall(2) + .calledWith( + zrxTokenAddress, + makerAddress, + feeRecipient, + bigNumberMatch(makerFee), + TradeSide.Maker, + TransferType.Fee, + ), + ).to.be.true(); + expect( + transferFromAsync + .getCall(3) + .calledWith( + zrxTokenAddress, + takerAddress, + feeRecipient, + bigNumberMatch(takerFee), + TradeSide.Taker, + TransferType.Fee, + ), + ).to.be.true(); + }); + it('should call exchangeTransferSimulator.transferFrom with correct values for an open order', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + takerTokenAddress, + makerFee, + takerFee, + makerAddress, + constants.NULL_ADDRESS, + fillableAmount, + feeRecipient, + ); + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, + signedOrder, + fillableAmount, + takerAddress, + zrxTokenAddress, + ); + expect(transferFromAsync.callCount).to.be.equal(4); + expect( + transferFromAsync + .getCall(0) + .calledWith( + makerTokenAddress, + makerAddress, + takerAddress, + bigNumberMatch(fillableAmount), + TradeSide.Maker, + TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync + .getCall(1) + .calledWith( + takerTokenAddress, + takerAddress, + makerAddress, + bigNumberMatch(fillableAmount), + TradeSide.Taker, + TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync + .getCall(2) + .calledWith( + zrxTokenAddress, + makerAddress, + feeRecipient, + bigNumberMatch(makerFee), + TradeSide.Maker, + TransferType.Fee, + ), + ).to.be.true(); + expect( + transferFromAsync + .getCall(3) + .calledWith( + zrxTokenAddress, + takerAddress, + feeRecipient, + bigNumberMatch(takerFee), + TradeSide.Taker, + TransferType.Fee, + ), + ).to.be.true(); + }); + it('should correctly round the fillMakerTokenAmount', async () => { + const makerTokenAmount = new BigNumber(3); + const takerTokenAmount = new BigNumber(1); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, + takerTokenAddress, + makerAddress, + takerAddress, + makerTokenAmount, + takerTokenAmount, + ); + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, + signedOrder, + takerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + expect(transferFromAsync.callCount).to.be.equal(4); + const makerFillAmount = transferFromAsync.getCall(0).args[3]; + expect(makerFillAmount).to.be.bignumber.equal(makerTokenAmount); + }); + it('should correctly round the makerFeeAmount', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(4); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, + takerTokenAddress, + makerFee, + takerFee, + makerAddress, + takerAddress, + fillableAmount, + constants.NULL_ADDRESS, + ); + const fillTakerTokenAmount = fillableAmount.div(2).round(0); + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, + signedOrder, + fillTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + const makerPartialFee = makerFee.div(2); + const takerPartialFee = takerFee.div(2); + expect(transferFromAsync.callCount).to.be.equal(4); + const partialMakerFee = transferFromAsync.getCall(2).args[3]; + expect(partialMakerFee).to.be.bignumber.equal(makerPartialFee); + const partialTakerFee = transferFromAsync.getCall(3).args[3]; + expect(partialTakerFee).to.be.bignumber.equal(takerPartialFee); + }); + }); +}); // tslint:disable-line:max-file-line-count diff --git a/packages/contract-wrappers/test/subscription_test.ts b/packages/contract-wrappers/test/subscription_test.ts new file mode 100644 index 000000000..64262ad9c --- /dev/null +++ b/packages/contract-wrappers/test/subscription_test.ts @@ -0,0 +1,95 @@ +import { BlockchainLifecycle, callbackErrorReporter, devConstants } from '@0xproject/dev-utils'; +import { DoneCallback } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import 'mocha'; +import * as Sinon from 'sinon'; + +import { ApprovalContractEventArgs, ContractWrappers, DecodedLogEvent, Token, TokenEvents } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('SubscriptionTest', () => { + let contractWrappers: ContractWrappers; + let userAddresses: string[]; + let tokens: Token[]; + let coinbase: string; + let addressWithoutFunds: string; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + coinbase = userAddresses[0]; + addressWithoutFunds = userAddresses[1]; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + let tokenAddress: string; + const allowanceAmount = new BigNumber(42); + let stubs: Sinon.SinonStub[] = []; + before(() => { + const token = tokens[0]; + tokenAddress = token.address; + }); + afterEach(() => { + contractWrappers.token.unsubscribeAll(); + _.each(stubs, s => s.restore()); + stubs = []; + }); + it('Should receive the Error when an error occurs while fetching the block', (done: DoneCallback) => { + (async () => { + const errMsg = 'Error fetching block'; + const callback = callbackErrorReporter.assertNodeCallbackError(done, errMsg); + stubs = [Sinon.stub((contractWrappers as any)._web3Wrapper, 'getBlockAsync').throws(new Error(errMsg))]; + contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback); + await contractWrappers.token.setAllowanceAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + allowanceAmount, + ); + })().catch(done); + }); + it('Should receive the Error when an error occurs while reconciling the new block', (done: DoneCallback) => { + (async () => { + const errMsg = 'Error fetching logs'; + const callback = callbackErrorReporter.assertNodeCallbackError(done, errMsg); + stubs = [Sinon.stub((contractWrappers as any)._web3Wrapper, 'getLogsAsync').throws(new Error(errMsg))]; + contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback); + await contractWrappers.token.setAllowanceAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + allowanceAmount, + ); + })().catch(done); + }); + it('Should allow unsubscribeAll to be called successfully after an error', (done: DoneCallback) => { + (async () => { + const callback = (err: Error | null, logEvent?: DecodedLogEvent<ApprovalContractEventArgs>) => _.noop; + contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback); + stubs = [ + Sinon.stub((contractWrappers as any)._web3Wrapper, 'getBlockAsync').throws( + new Error('JSON RPC error'), + ), + ]; + contractWrappers.token.unsubscribeAll(); + done(); + })().catch(done); + }); + }); +}); diff --git a/packages/contract-wrappers/test/token_registry_wrapper_test.ts b/packages/contract-wrappers/test/token_registry_wrapper_test.ts new file mode 100644 index 000000000..a21efed21 --- /dev/null +++ b/packages/contract-wrappers/test/token_registry_wrapper_test.ts @@ -0,0 +1,136 @@ +import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils'; +import { schemas, SchemaValidator } from '@0xproject/json-schemas'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { ContractWrappers, Token } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const TOKEN_REGISTRY_SIZE_AFTER_MIGRATION = 7; + +describe('TokenRegistryWrapper', () => { + let contractWrappers: ContractWrappers; + let tokens: Token[]; + const tokenAddressBySymbol: { [symbol: string]: string } = {}; + const tokenAddressByName: { [symbol: string]: string } = {}; + const tokenBySymbol: { [symbol: string]: Token } = {}; + const tokenByName: { [symbol: string]: Token } = {}; + const registeredSymbol = 'ZRX'; + const registeredName = '0x Protocol Token'; + const unregisteredSymbol = 'MAL'; + const unregisteredName = 'Malicious Token'; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + _.map(tokens, token => { + tokenAddressBySymbol[token.symbol] = token.address; + tokenAddressByName[token.name] = token.address; + tokenBySymbol[token.symbol] = token; + tokenByName[token.name] = token; + }); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#getTokensAsync', () => { + it('should return all the tokens added to the tokenRegistry during the migration', async () => { + expect(tokens).to.have.lengthOf(TOKEN_REGISTRY_SIZE_AFTER_MIGRATION); + + const schemaValidator = new SchemaValidator(); + _.each(tokens, token => { + const validationResult = schemaValidator.validate(token, schemas.tokenSchema); + expect(validationResult.errors).to.have.lengthOf(0); + }); + }); + }); + describe('#getTokenAddressesAsync', () => { + it('should return all the token addresses added to the tokenRegistry during the migration', async () => { + const tokenAddresses = await contractWrappers.tokenRegistry.getTokenAddressesAsync(); + expect(tokenAddresses).to.have.lengthOf(TOKEN_REGISTRY_SIZE_AFTER_MIGRATION); + + const schemaValidator = new SchemaValidator(); + _.each(tokenAddresses, tokenAddress => { + const validationResult = schemaValidator.validate(tokenAddress, schemas.addressSchema); + expect(validationResult.errors).to.have.lengthOf(0); + expect(tokenAddress).to.not.be.equal(constants.NULL_ADDRESS); + }); + }); + }); + describe('#getTokenAddressBySymbol', () => { + it('should return correct address for a token in the registry', async () => { + const tokenAddress = await contractWrappers.tokenRegistry.getTokenAddressBySymbolIfExistsAsync( + registeredSymbol, + ); + expect(tokenAddress).to.be.equal(tokenAddressBySymbol[registeredSymbol]); + }); + it('should return undefined for a token out of registry', async () => { + const tokenAddress = await contractWrappers.tokenRegistry.getTokenAddressBySymbolIfExistsAsync( + unregisteredSymbol, + ); + expect(tokenAddress).to.be.undefined(); + }); + }); + describe('#getTokenAddressByName', () => { + it('should return correct address for a token in the registry', async () => { + const tokenAddress = await contractWrappers.tokenRegistry.getTokenAddressByNameIfExistsAsync( + registeredName, + ); + expect(tokenAddress).to.be.equal(tokenAddressByName[registeredName]); + }); + it('should return undefined for a token out of registry', async () => { + const tokenAddress = await contractWrappers.tokenRegistry.getTokenAddressByNameIfExistsAsync( + unregisteredName, + ); + expect(tokenAddress).to.be.undefined(); + }); + }); + describe('#getTokenBySymbol', () => { + it('should return correct token for a token in the registry', async () => { + const token = await contractWrappers.tokenRegistry.getTokenBySymbolIfExistsAsync(registeredSymbol); + expect(token).to.be.deep.equal(tokenBySymbol[registeredSymbol]); + }); + it('should return undefined for a token out of registry', async () => { + const token = await contractWrappers.tokenRegistry.getTokenBySymbolIfExistsAsync(unregisteredSymbol); + expect(token).to.be.undefined(); + }); + }); + describe('#getTokenByName', () => { + it('should return correct token for a token in the registry', async () => { + const token = await contractWrappers.tokenRegistry.getTokenByNameIfExistsAsync(registeredName); + expect(token).to.be.deep.equal(tokenByName[registeredName]); + }); + it('should return undefined for a token out of registry', async () => { + const token = await contractWrappers.tokenRegistry.getTokenByNameIfExistsAsync(unregisteredName); + expect(token).to.be.undefined(); + }); + }); + describe('#getTokenIfExistsAsync', () => { + it('should return the token added to the tokenRegistry during the migration', async () => { + const aToken = tokens[0]; + + const token = await contractWrappers.tokenRegistry.getTokenIfExistsAsync(aToken.address); + const schemaValidator = new SchemaValidator(); + const validationResult = schemaValidator.validate(token, schemas.tokenSchema); + expect(validationResult.errors).to.have.lengthOf(0); + }); + it('should return return undefined when passed a token address not in the tokenRegistry', async () => { + const unregisteredTokenAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; + const tokenIfExists = await contractWrappers.tokenRegistry.getTokenIfExistsAsync(unregisteredTokenAddress); + expect(tokenIfExists).to.be.undefined(); + }); + }); +}); diff --git a/packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts b/packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts new file mode 100644 index 000000000..0b66985aa --- /dev/null +++ b/packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts @@ -0,0 +1,35 @@ +import * as chai from 'chai'; + +import { ContractWrappers } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { provider } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('TokenTransferProxyWrapper', () => { + let contractWrappers: ContractWrappers; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + }); + describe('#isAuthorizedAsync', () => { + it('should return false if the address is not authorized', async () => { + const isAuthorized = await contractWrappers.proxy.isAuthorizedAsync(constants.NULL_ADDRESS); + expect(isAuthorized).to.be.false(); + }); + }); + describe('#getAuthorizedAddressesAsync', () => { + it('should return the list of authorized addresses', async () => { + const authorizedAddresses = await contractWrappers.proxy.getAuthorizedAddressesAsync(); + for (const authorizedAddress of authorizedAddresses) { + const isAuthorized = await contractWrappers.proxy.isAuthorizedAsync(authorizedAddress); + expect(isAuthorized).to.be.true(); + } + }); + }); +}); diff --git a/packages/contract-wrappers/test/token_wrapper_test.ts b/packages/contract-wrappers/test/token_wrapper_test.ts new file mode 100644 index 000000000..053901c85 --- /dev/null +++ b/packages/contract-wrappers/test/token_wrapper_test.ts @@ -0,0 +1,605 @@ +import { BlockchainLifecycle, callbackErrorReporter, devConstants } from '@0xproject/dev-utils'; +import { EmptyWalletSubprovider } from '@0xproject/subproviders'; +import { DoneCallback, Provider } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; +import Web3ProviderEngine = require('web3-provider-engine'); + +import { + ApprovalContractEventArgs, + BlockParamLiteral, + BlockRange, + ContractWrappers, + ContractWrappersError, + DecodedLogEvent, + Token, + TokenEvents, + TransferContractEventArgs, +} from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { TokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('TokenWrapper', () => { + let contractWrappers: ContractWrappers; + let userAddresses: string[]; + let tokens: Token[]; + let tokenUtils: TokenUtils; + let coinbase: string; + let addressWithoutFunds: string; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + tokens = await contractWrappers.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + coinbase = userAddresses[0]; + addressWithoutFunds = userAddresses[1]; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#transferAsync', () => { + let token: Token; + let transferAmount: BigNumber; + before(() => { + token = tokens[0]; + transferAmount = new BigNumber(42); + }); + it('should successfully transfer tokens', async () => { + const fromAddress = coinbase; + const toAddress = addressWithoutFunds; + const preBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); + expect(preBalance).to.be.bignumber.equal(0); + await contractWrappers.token.transferAsync(token.address, fromAddress, toAddress, transferAmount); + const postBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); + return expect(postBalance).to.be.bignumber.equal(transferAmount); + }); + it('should fail to transfer tokens if fromAddress has an insufficient balance', async () => { + const fromAddress = addressWithoutFunds; + const toAddress = coinbase; + return expect( + contractWrappers.token.transferAsync(token.address, fromAddress, toAddress, transferAmount), + ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + const fromAddress = coinbase; + const toAddress = coinbase; + return expect( + contractWrappers.token.transferAsync(nonExistentTokenAddress, fromAddress, toAddress, transferAmount), + ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist); + }); + }); + describe('#transferFromAsync', () => { + let token: Token; + let toAddress: string; + let senderAddress: string; + before(async () => { + token = tokens[0]; + toAddress = addressWithoutFunds; + senderAddress = userAddresses[2]; + }); + it('should fail to transfer tokens if fromAddress has insufficient allowance set', async () => { + const fromAddress = coinbase; + const transferAmount = new BigNumber(42); + + const fromAddressBalance = await contractWrappers.token.getBalanceAsync(token.address, fromAddress); + expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount); + + const fromAddressAllowance = await contractWrappers.token.getAllowanceAsync( + token.address, + fromAddress, + toAddress, + ); + expect(fromAddressAllowance).to.be.bignumber.equal(0); + + return expect( + contractWrappers.token.transferFromAsync( + token.address, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer); + }); + it('[regression] should fail to transfer tokens if set allowance for toAddress instead of senderAddress', async () => { + const fromAddress = coinbase; + const transferAmount = new BigNumber(42); + + await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, toAddress, transferAmount); + + return expect( + contractWrappers.token.transferFromAsync( + token.address, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer); + }); + it('should fail to transfer tokens if fromAddress has insufficient balance', async () => { + const fromAddress = addressWithoutFunds; + const transferAmount = new BigNumber(42); + + const fromAddressBalance = await contractWrappers.token.getBalanceAsync(token.address, fromAddress); + expect(fromAddressBalance).to.be.bignumber.equal(0); + + await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount); + const fromAddressAllowance = await contractWrappers.token.getAllowanceAsync( + token.address, + fromAddress, + senderAddress, + ); + expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount); + + return expect( + contractWrappers.token.transferFromAsync( + token.address, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer); + }); + it('should successfully transfer tokens', async () => { + const fromAddress = coinbase; + + const preBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); + expect(preBalance).to.be.bignumber.equal(0); + + const transferAmount = new BigNumber(42); + await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount); + + await contractWrappers.token.transferFromAsync( + token.address, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ); + const postBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); + return expect(postBalance).to.be.bignumber.equal(transferAmount); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const fromAddress = coinbase; + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + return expect( + contractWrappers.token.transferFromAsync( + nonExistentTokenAddress, + fromAddress, + toAddress, + senderAddress, + new BigNumber(42), + ), + ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist); + }); + }); + describe('#getBalanceAsync', () => { + describe('With provider with accounts', () => { + it('should return the balance for an existing ERC20 token', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const balance = await contractWrappers.token.getBalanceAsync(token.address, ownerAddress); + const expectedBalance = new BigNumber('1000000000000000000000000000'); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + const ownerAddress = coinbase; + return expect( + contractWrappers.token.getBalanceAsync(nonExistentTokenAddress, ownerAddress), + ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist); + }); + it('should return a balance of 0 for a non-existent owner address', async () => { + const token = tokens[0]; + const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593'; + const balance = await contractWrappers.token.getBalanceAsync(token.address, nonExistentOwner); + const expectedBalance = new BigNumber(0); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + }); + describe('With provider without accounts', () => { + let zeroExContractWithoutAccounts: ContractWrappers; + before(async () => { + const hasAddresses = false; + const emptyWalletProvider = addEmptyWalletSubprovider(provider); + zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); + }); + it('should return balance even when called with provider instance without addresses', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const balance = await zeroExContractWithoutAccounts.token.getBalanceAsync(token.address, ownerAddress); + const expectedBalance = new BigNumber('1000000000000000000000000000'); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + }); + }); + describe('#setAllowanceAsync', () => { + it("should set the spender's allowance", async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + const allowanceBeforeSet = await contractWrappers.token.getAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + ); + const expectedAllowanceBeforeAllowanceSet = new BigNumber(0); + expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet); + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.token.setAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + amountInBaseUnits, + ); + + const allowanceAfterSet = await contractWrappers.token.getAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + ); + const expectedAllowanceAfterAllowanceSet = amountInBaseUnits; + return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet); + }); + }); + describe('#setUnlimitedAllowanceAsync', () => { + it("should set the unlimited spender's allowance", async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + await contractWrappers.token.setUnlimitedAllowanceAsync(token.address, ownerAddress, spenderAddress); + const allowance = await contractWrappers.token.getAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + ); + return expect(allowance).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + }); + it('should reduce the gas cost for transfers including tokens with unlimited allowance support', async () => { + const transferAmount = new BigNumber(5); + const zrx = tokenUtils.getProtocolTokenOrThrow(); + const [, userWithNormalAllowance, userWithUnlimitedAllowance] = userAddresses; + await contractWrappers.token.setAllowanceAsync( + zrx.address, + coinbase, + userWithNormalAllowance, + transferAmount, + ); + await contractWrappers.token.setUnlimitedAllowanceAsync(zrx.address, coinbase, userWithUnlimitedAllowance); + + const initBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance); + const initBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync( + userWithUnlimitedAllowance, + ); + + await contractWrappers.token.transferFromAsync( + zrx.address, + coinbase, + userWithNormalAllowance, + userWithNormalAllowance, + transferAmount, + ); + await contractWrappers.token.transferFromAsync( + zrx.address, + coinbase, + userWithUnlimitedAllowance, + userWithUnlimitedAllowance, + transferAmount, + ); + + const finalBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance); + const finalBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync( + userWithUnlimitedAllowance, + ); + + const normalGasCost = initBalanceWithNormalAllowance.minus(finalBalanceWithNormalAllowance); + const unlimitedGasCost = initBalanceWithUnlimitedAllowance.minus(finalBalanceWithUnlimitedAllowance); + + // In theory the gas cost with unlimited allowance should be smaller, but with testrpc it's actually bigger. + // This needs to be investigated in ethereumjs-vm. This test is essentially a repro. + // TODO: Make this test pass with inverted assertion. + expect(unlimitedGasCost.toNumber()).to.be.gt(normalGasCost.toNumber()); + }); + }); + describe('#getAllowanceAsync', () => { + describe('With provider with accounts', () => { + it('should get the proxy allowance', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.token.setAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + amountInBaseUnits, + ); + + const allowance = await contractWrappers.token.getAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + ); + const expectedAllowance = amountInBaseUnits; + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + it('should return 0 if no allowance set yet', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + const allowance = await contractWrappers.token.getAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + ); + const expectedAllowance = new BigNumber(0); + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + }); + describe('With provider without accounts', () => { + let zeroExContractWithoutAccounts: ContractWrappers; + before(async () => { + const hasAddresses = false; + const emptyWalletProvider = addEmptyWalletSubprovider(provider); + zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); + }); + it('should get the proxy allowance', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.token.setAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + amountInBaseUnits, + ); + + const allowance = await zeroExContractWithoutAccounts.token.getAllowanceAsync( + token.address, + ownerAddress, + spenderAddress, + ); + const expectedAllowance = amountInBaseUnits; + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + }); + }); + describe('#getProxyAllowanceAsync', () => { + it('should get the proxy allowance', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits); + + const allowance = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); + const expectedAllowance = amountInBaseUnits; + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + }); + describe('#setProxyAllowanceAsync', () => { + it('should set the proxy allowance', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + + const allowanceBeforeSet = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); + const expectedAllowanceBeforeAllowanceSet = new BigNumber(0); + expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet); + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits); + + const allowanceAfterSet = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); + const expectedAllowanceAfterAllowanceSet = amountInBaseUnits; + return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet); + }); + }); + describe('#setUnlimitedProxyAllowanceAsync', () => { + it('should set the unlimited proxy allowance', async () => { + const token = tokens[0]; + const ownerAddress = coinbase; + + await contractWrappers.token.setUnlimitedProxyAllowanceAsync(token.address, ownerAddress); + const allowance = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); + return expect(allowance).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + }); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + let tokenAddress: string; + const transferAmount = new BigNumber(42); + const allowanceAmount = new BigNumber(42); + before(() => { + const token = tokens[0]; + tokenAddress = token.address; + }); + afterEach(() => { + contractWrappers.token.unsubscribeAll(); + }); + // Hack: Mocha does not allow a test to be both async and have a `done` callback + // Since we need to await the receipt of the event in the `subscribe` callback, + // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then + // wrap the rest of the test in an async block + // Source: https://github.com/mochajs/mocha/issues/2407 + it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<TransferContractEventArgs>) => { + expect(logEvent.isRemoved).to.be.false(); + expect(logEvent.log.logIndex).to.be.equal(0); + expect(logEvent.log.transactionIndex).to.be.equal(0); + expect(logEvent.log.blockNumber).to.be.a('number'); + const args = logEvent.log.args; + expect(args._from).to.be.equal(coinbase); + expect(args._to).to.be.equal(addressWithoutFunds); + expect(args._value).to.be.bignumber.equal(transferAmount); + }, + ); + contractWrappers.token.subscribe(tokenAddress, TokenEvents.Transfer, indexFilterValues, callback); + await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); + })().catch(done); + }); + it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; + expect(args._owner).to.be.equal(coinbase); + expect(args._spender).to.be.equal(addressWithoutFunds); + expect(args._value).to.be.bignumber.equal(allowanceAmount); + }, + ); + contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback); + await contractWrappers.token.setAllowanceAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + allowanceAmount, + ); + })().catch(done); + }); + it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + contractWrappers.token.subscribe( + tokenAddress, + TokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(); + contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); + contractWrappers.token.subscribe( + tokenAddress, + TokenEvents.Transfer, + indexFilterValues, + callbackToBeCalled, + ); + await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); + })().catch(done); + }); + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + const subscriptionToken = contractWrappers.token.subscribe( + tokenAddress, + TokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + contractWrappers.token.unsubscribe(subscriptionToken); + await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); + done(); + })().catch(done); + }); + }); + describe('#getLogsAsync', () => { + let tokenAddress: string; + let tokenTransferProxyAddress: string; + const blockRange: BlockRange = { + fromBlock: 0, + toBlock: BlockParamLiteral.Latest, + }; + let txHash: string; + before(() => { + const token = tokens[0]; + tokenAddress = token.address; + tokenTransferProxyAddress = contractWrappers.proxy.getContractAddress(); + }); + it('should get logs with decoded args emitted by Approval', async () => { + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const eventName = TokenEvents.Approval; + const indexFilterValues = {}; + const logs = await contractWrappers.token.getLogsAsync<ApprovalContractEventArgs>( + tokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(logs[0].event).to.be.equal(eventName); + expect(args._owner).to.be.equal(coinbase); + expect(args._spender).to.be.equal(tokenTransferProxyAddress); + expect(args._value).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + }); + it('should only get the logs with the correct event name', async () => { + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const differentEventName = TokenEvents.Transfer; + const indexFilterValues = {}; + const logs = await contractWrappers.token.getLogsAsync( + tokenAddress, + differentEventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(0); + }); + it('should only get the logs with the correct indexed fields', async () => { + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, addressWithoutFunds); + await web3Wrapper.awaitTransactionMinedAsync(txHash); + const eventName = TokenEvents.Approval; + const indexFilterValues = { + _owner: coinbase, + }; + const logs = await contractWrappers.token.getLogsAsync<ApprovalContractEventArgs>( + tokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(args._owner).to.be.equal(coinbase); + }); + }); +}); +// tslint:disable:max-file-line-count + +function addEmptyWalletSubprovider(p: Provider): Provider { + const providerEngine = new Web3ProviderEngine(); + providerEngine.addProvider(new EmptyWalletSubprovider()); + const currentSubproviders = (p as any)._providers; + for (const subprovider of currentSubproviders) { + providerEngine.addProvider(subprovider); + } + providerEngine.start(); + return providerEngine; +} diff --git a/packages/contract-wrappers/test/utils/chai_setup.ts b/packages/contract-wrappers/test/utils/chai_setup.ts new file mode 100644 index 000000000..078edd309 --- /dev/null +++ b/packages/contract-wrappers/test/utils/chai_setup.ts @@ -0,0 +1,13 @@ +import * as chai from 'chai'; +import chaiAsPromised = require('chai-as-promised'); +import ChaiBigNumber = require('chai-bignumber'); +import * as dirtyChai from 'dirty-chai'; + +export const chaiSetup = { + configure() { + chai.config.includeStack = true; + chai.use(ChaiBigNumber()); + chai.use(dirtyChai); + chai.use(chaiAsPromised); + }, +}; diff --git a/packages/contract-wrappers/test/utils/constants.ts b/packages/contract-wrappers/test/utils/constants.ts new file mode 100644 index 000000000..cf030259c --- /dev/null +++ b/packages/contract-wrappers/test/utils/constants.ts @@ -0,0 +1,9 @@ +export const constants = { + NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + ROPSTEN_NETWORK_ID: 3, + KOVAN_NETWORK_ID: 42, + TESTRPC_NETWORK_ID: 50, + KOVAN_RPC_URL: 'https://kovan.infura.io/', + ROPSTEN_RPC_URL: 'https://ropsten.infura.io/', + ZRX_DECIMALS: 18, +}; diff --git a/packages/contract-wrappers/test/utils/token_utils.ts b/packages/contract-wrappers/test/utils/token_utils.ts new file mode 100644 index 000000000..fe85de085 --- /dev/null +++ b/packages/contract-wrappers/test/utils/token_utils.ts @@ -0,0 +1,33 @@ +import * as _ from 'lodash'; + +import { InternalContractWrappersError, Token } from '../../src/types'; + +const PROTOCOL_TOKEN_SYMBOL = 'ZRX'; +const WETH_TOKEN_SYMBOL = 'WETH'; + +export class TokenUtils { + private _tokens: Token[]; + constructor(tokens: Token[]) { + this._tokens = tokens; + } + public getProtocolTokenOrThrow(): Token { + const zrxToken = _.find(this._tokens, { symbol: PROTOCOL_TOKEN_SYMBOL }); + if (_.isUndefined(zrxToken)) { + throw new Error(InternalContractWrappersError.ZrxNotInTokenRegistry); + } + return zrxToken; + } + public getWethTokenOrThrow(): Token { + const wethToken = _.find(this._tokens, { symbol: WETH_TOKEN_SYMBOL }); + if (_.isUndefined(wethToken)) { + throw new Error(InternalContractWrappersError.WethNotInTokenRegistry); + } + return wethToken; + } + public getDummyTokens(): Token[] { + const dummyTokens = _.filter(this._tokens, token => { + return !_.includes([PROTOCOL_TOKEN_SYMBOL, WETH_TOKEN_SYMBOL], token.symbol); + }); + return dummyTokens; + } +} diff --git a/packages/contract-wrappers/test/utils/web3_wrapper.ts b/packages/contract-wrappers/test/utils/web3_wrapper.ts new file mode 100644 index 000000000..b0ccfa546 --- /dev/null +++ b/packages/contract-wrappers/test/utils/web3_wrapper.ts @@ -0,0 +1,9 @@ +import { devConstants, web3Factory } from '@0xproject/dev-utils'; +import { Provider } from '@0xproject/types'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; + +const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); +const provider: Provider = web3.currentProvider; +const web3Wrapper = new Web3Wrapper(web3.currentProvider); + +export { provider, web3Wrapper }; |